summaryrefslogtreecommitdiffstats
path: root/src/VBox/ValidationKit/testmanager
diff options
context:
space:
mode:
Diffstat (limited to 'src/VBox/ValidationKit/testmanager')
-rw-r--r--src/VBox/ValidationKit/testmanager/Makefile.kmk54
-rw-r--r--src/VBox/ValidationKit/testmanager/__init__.py40
-rw-r--r--src/VBox/ValidationKit/testmanager/apache-template-2.2.conf87
-rw-r--r--src/VBox/ValidationKit/testmanager/apache-template-2.4.conf81
-rw-r--r--src/VBox/ValidationKit/testmanager/batch/Makefile.kmk46
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/add_build.py137
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/check_for_deleted_builds.py133
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/close_orphaned_testsets.py105
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/del_build.py95
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/filearchiver.py282
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/quota.py319
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/regen_sched_queues.py132
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/vcs_import.py205
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/batch/virtual_test_sheriff.py1832
-rw-r--r--src/VBox/ValidationKit/testmanager/cgi/Makefile.kmk46
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/cgi/admin.py77
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/cgi/debuginfo.py71
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/cgi/index.py78
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/cgi/logout.py79
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/cgi/logout2.py81
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/cgi/rest.py81
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/cgi/status.py519
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/cgi/testboxdisp.py75
-rw-r--r--src/VBox/ValidationKit/testmanager/config.py261
-rw-r--r--src/VBox/ValidationKit/testmanager/core/Makefile.kmk46
-rw-r--r--src/VBox/ValidationKit/testmanager/core/__init__.py40
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/base.py1514
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/build.py891
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/buildblacklist.py324
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/buildsource.py524
-rw-r--r--src/VBox/ValidationKit/testmanager/core/coreconsts.py100
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/db.py745
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/dbobjcache.py200
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/failurecategory.py392
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/failurereason.py580
-rw-r--r--src/VBox/ValidationKit/testmanager/core/globalresource.pgsql118
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/globalresource.py328
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/report.py1307
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/restdispatcher.py455
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/schedgroup.py1352
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/schedqueue.py153
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/schedulerbase.py1570
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/schedulerbeci.py128
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/systemchangelog.py202
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/systemlog.py186
-rw-r--r--src/VBox/ValidationKit/testmanager/core/testbox.pgsql635
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testbox.py1286
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testboxcontroller.py954
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testboxstatus.py317
-rw-r--r--src/VBox/ValidationKit/testmanager/core/testcase.pgsql275
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testcase.py1467
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testcaseargs.py416
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testgroup.py771
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testresultfailures.py529
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testresults.py2926
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/testset.py869
-rw-r--r--src/VBox/ValidationKit/testmanager/core/useraccount.pgsql178
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/useraccount.py302
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/vcsbugreference.py251
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/vcsrevisions.py254
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/webservergluebase.py717
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/core/webservergluecgi.py100
-rw-r--r--src/VBox/ValidationKit/testmanager/db/Makefile.kmk98
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase.dmd8
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/DataTypes.xml15
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/47E390DE-0671-C4B1-8428-0F45CBEE18F8.xml37
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1.xml37
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/subviews/E9476B45-3C62-EE27-4705-6F1EFAD11B74.xml21
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultRDBMSSites.xml12
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultdomains.xml13
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dl_settings.xml288
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dr_custom_scripts.xml360
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/Logical.xml7
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/16464F5A-64BE-D2ED-91E0-BCBD0AA34680.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/1BEAB532-23CA-8628-0C97-7CAD39119A4E.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/24150FB1-B00F-4F69-6F77-49ECB58F0F66.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/28DD93CF-D058-7343-CD47-E9B435E1AC16.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/2F6ACC6D-3D17-537D-8ADF-F8424395B345.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4579B792-2F35-D72A-1A3B-C7E53C41A766.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4D937E7C-3A28-E52D-89C0-EC8804C62367.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/504221DA-1B57-4EAD-39DB-40FD553E9FA2.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/6A886CEE-579B-48FF-63F6-0FB03393FBF6.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/7AE36CC1-A030-63E5-6EF3-72FCD04815EE.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90367AFB-BA2D-A918-46B9-1E5DE53ACC48.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90F477EE-35D6-21A7-B693-E5724FB07476.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/9F78B73C-056D-DDEF-8C50-A9DA76B9E724.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A352A20F-310D-E285-FBC9-90DD0DA7BB9B.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A6A5F317-479C-A0DD-CAAE-9DCB56B29D40.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B36A186B-CDB3-7851-8C38-12EA8D50EAEB.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B82DAF9A-6F99-5CF6-4D99-A391BAD66192.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C332E3D7-638B-6CA8-24BF-383CA8659A3A.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C79482B8-771B-FAD8-0337-163E3A45003A.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/D09E0DE5-99D6-2991-032A-A8A124F6ACBA.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DCC79294-5434-1DED-298C-6473DEE59FBA.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DE366053-6F7A-7F42-ABA3-00E583098C37.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/E93BBF08-067B-A665-39F3-CF488A6547B2.xml52
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/876CB767-80BA-6C8E-AACA-F1CCC95C445E.xml16
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/D487AFDC-4027-F824-EA29-5C6D0ABB9E1E.xml16
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/01537211-CCFB-0A1E-B43B-E8C641B69471.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/02096BBB-0795-1759-1E26-2877BE36BB59.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/0CCF1DE3-7916-9054-BEA6-C601FF564DB2.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/10867E70-94CE-FDAF-6B6E-2742D3A49E57.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/11710A55-6423-1904-841A-C7D2AB8CEEBF.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/1C189437-742B-B999-C955-7754C8ADB089.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/34733942-1305-4CA1-47EB-ACE724B04E69.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3563C940-E524-7F96-7AE0-DAC3C1C17AFC.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3B7C8913-EB6A-47B1-27D0-E2C85EE9048B.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/518CE489-97B4-C05C-07A2-E3DBF14EE267.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/68A0C3E1-0FA1-8414-A361-33B08A8EDB39.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7497D76B-781B-3BDD-D797-FFBDB974F772.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/89A83E25-364B-6B73-0613-FEAD875EF9FB.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/A182A65A-47AE-5D00-9A30-BC20AB050BF2.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B346381F-48FE-E495-01A7-E22EC26AEE8A.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B3596116-540F-6397-ECE4-58A386644E15.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/BAD8EC05-6F14-4E38-366C-B4B660C6F38A.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/CCD38E11-8557-EB34-2651-07EB29E83FA6.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E2A47942-ED55-E81D-4C71-9A134C49C147.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E4FE88E9-EE21-B43B-B0FE-A153E38246F9.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E62AE7DF-49EE-9280-B328-A867CBD273AE.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E74406B5-20F1-4323-DC99-6E45982CB606.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EC4EB506-3DBE-7F36-6451-F31920EDAB52.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880.xml17
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF.xml40
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/32D718B4-250F-95DC-37F0-C0A817F69020.xml70
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/571DBBAF-CDDA-1C46-4220-D1319C0EEC00.xml24
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83.xml127
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/AFCEF013-4CF2-4A5A-79A3-31521C1CA20A.xml306
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/F936BE6D-7A74-1B57-7564-41C1E13B973B.xml33
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap.xml3
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap_RMB082B14A-BEA8-D8A7-D661-197F34766ED3.xml3
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rdbms/TestManagerDatabase_RDBMSSites.xml2
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3.xml8
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3/subviews/6CEC5843-B4DD-D9B0-54D4-2845569D5E9F.xml13
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/types.xml933
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseComments.pgsql1193
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseDefaultUserAccounts.pgsql43
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks.pgsql90
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks2.pgsql77
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseInit.pgsql1950
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseMap.pngbin0 -> 44750 bytes
-rw-r--r--src/VBox/ValidationKit/testmanager/db/TestManagerVBoxPilot-1.pgsql101
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/db/gen-sql-comments.py236
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/db/partial-db-dump.py392
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r01-builds-1.pgsql91
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r02-testboxes-1.pgsql194
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r03-teststatus-1.pgsql48
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r04-teststatus-2.pgsql46
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r05-teststatus-3.pgsql54
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r06-buildsources-1.pgsql46
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r07-testresults-1.pgsql47
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r08-testresultvalues-1.pgsql47
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r09-testsets-1.pgsql48
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r10-testresultvalues-2.pgsql111
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r11-testsets-2.pgsql214
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r12-testresultvalues-3-testsets-3.pgsql58
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql134
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r14-testboxes-2.pgsql201
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r15-index-sorting.pgsql108
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql122
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r17-testresultvalues-4.pgsql48
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql171
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r19-testboxes-3.pgsql356
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql67
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r21-testsets-4.pgsql290
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql181
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r23-users-2.pgsql60
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r24-vcsbugreferences-1.pgsql59
-rw-r--r--src/VBox/ValidationKit/testmanager/db/tmdb-r25-vcsrevisions-2.pgsql45
-rw-r--r--src/VBox/ValidationKit/testmanager/debug/Makefile.kmk46
-rw-r--r--src/VBox/ValidationKit/testmanager/debug/__init__.py40
-rw-r--r--src/VBox/ValidationKit/testmanager/debug/add_testbox.pgsql76
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/debug/cgiprofiling.py83
-rw-r--r--src/VBox/ValidationKit/testmanager/debug/functions.pgsql82
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup0
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/css/common.css1183
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/css/details.css216
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css237
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css132
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg806
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.pngbin0 -> 7884 bytes
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.icobin0 -> 3262 bytes
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup0
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/js/common.js1926
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js126
-rw-r--r--src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js237
-rw-r--r--src/VBox/ValidationKit/testmanager/misc/Makefile.kmk46
-rw-r--r--src/VBox/ValidationKit/testmanager/misc/htpasswd-logout1
-rw-r--r--src/VBox/ValidationKit/testmanager/misc/htpasswd-sample2
-rw-r--r--src/VBox/ValidationKit/testmanager/readme.txt125
-rw-r--r--src/VBox/ValidationKit/testmanager/selftest/st1-load.pgsql164
-rw-r--r--src/VBox/ValidationKit/testmanager/selftest/st1-unload.pgsql87
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/Makefile.kmk47
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/__init__.py40
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/template-details.html45
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html46
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/template-tooltip.html20
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/template.html65
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadmin.py1270
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py154
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py164
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py122
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py160
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py155
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py175
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py130
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py205
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py79
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py447
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py72
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py85
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py490
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py258
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py197
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py110
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuibase.py1245
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuicontentbase.py1290
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py660
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpform.py1111
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py128
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py131
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py376
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py341
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py163
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuilogviewer.py251
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuimain.py1344
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuireport.py817
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuitestresult.py965
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py110
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuivcshistory.py101
235 files changed, 58661 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testmanager/Makefile.kmk b/src/VBox/ValidationKit/testmanager/Makefile.kmk
new file mode 100644
index 00000000..de7c4cc0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/Makefile.kmk
@@ -0,0 +1,54 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+SUB_DEPTH = ../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+include $(PATH_SUB_CURRENT)/cgi/Makefile.kmk
+include $(PATH_SUB_CURRENT)/core/Makefile.kmk
+include $(PATH_SUB_CURRENT)/batch/Makefile.kmk
+include $(PATH_SUB_CURRENT)/debug/Makefile.kmk
+include $(PATH_SUB_CURRENT)/misc/Makefile.kmk
+include $(PATH_SUB_CURRENT)/webui/Makefile.kmk
+
+VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py)
+VBOX_VALIDATIONKIT_JS_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/htdocs/js/*.js)
+
+
+$(evalcall def_vbox_validationkit_process_python_sources)
+$(evalcall def_vbox_validationkit_process_js_sources)
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/testmanager/__init__.py b/src/VBox/ValidationKit/testmanager/__init__.py
new file mode 100644
index 00000000..71137716
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/__init__.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# $Id: __init__.py $
+
+"""
+Test Manager.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
diff --git a/src/VBox/ValidationKit/testmanager/apache-template-2.2.conf b/src/VBox/ValidationKit/testmanager/apache-template-2.2.conf
new file mode 100644
index 00000000..0064cfaa
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/apache-template-2.2.conf
@@ -0,0 +1,87 @@
+# $Id: apache-template-2.2.conf $
+## @file
+# Test Manager - Apache 2.2 configuration sample.
+#
+# Requires TestManagerRootDir to be set in the environment (envvars file for instance).
+#
+
+#
+# Copyright (C) 2012-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+
+<LocationMatch "^/testmanager/logout.py">
+ AuthType Basic
+ AuthName "Test Manager"
+ AuthUserFile ${TestManagerRootDir}/misc/htpasswd-logout
+ Require user logout
+</LocationMatch>
+
+<LocationMatch "^/testmanager/(?!(testboxdisp.py|logout.py|/*htdocs/downloads/.*))">
+ AuthType Basic
+ AuthName "Test Manager"
+ AuthUserFile ${TestManagerRootDir}/misc/htpasswd-sample
+ Require valid-user
+</LocationMatch>
+
+# These two directives are only for local testing!
+Alias /testmanager/htdocs/downloads/VBoxValidationKit.zip ${VBoxBuildOutputDir}/VBoxValidationKit.zip
+<Location /testmanager/htdocs/downloads/VBoxValidationKit.zip>
+ Options Indexes
+ Order allow,deny
+ Allow from all
+</Location>
+
+Alias /testmanager/htdocs/ ${TestManagerRootDir}/htdocs/
+<Directory ${TestManagerRootDir}/htdocs/>
+ AllowOverride None
+ Options Indexes
+ Order allow,deny
+ Allow from all
+</Directory>
+
+Alias /testmanager/logs/ /var/tmp/testmanager/
+<Directory /var/tmp/testmanager/>
+ AllowOverride None
+ Options Indexes
+ Order allow,deny
+ Allow from all
+</Directory>
+
+Alias /testmanager/ ${TestManagerRootDir}/cgi/
+<Directory ${TestManagerRootDir}/cgi/>
+ AllowOverride None
+ Options Indexes ExecCGI
+ DirectoryIndex index.py
+ AddHandler cgi-script .py
+ Order allow,deny
+ Allow from all
+</Directory>
+
diff --git a/src/VBox/ValidationKit/testmanager/apache-template-2.4.conf b/src/VBox/ValidationKit/testmanager/apache-template-2.4.conf
new file mode 100644
index 00000000..e38c5924
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/apache-template-2.4.conf
@@ -0,0 +1,81 @@
+# $Id: apache-template-2.4.conf $
+## @file
+# Test Manager - Apache 2.4 configuration sample.
+#
+# Use the new Define directive to define TestManagerRootDir and
+# VBoxBuildOutputDir before including this file.
+#
+
+#
+# Copyright (C) 2012-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+
+<LocationMatch "^/testmanager/logout.py">
+ AuthType Basic
+ AuthName "Test Manager"
+ AuthUserFile ${TestManagerRootDir}/misc/htpasswd-logout
+ Require user logout
+</LocationMatch>
+
+<LocationMatch "^/testmanager/(?!(testboxdisp.py|logout.py|/*htdocs/downloads/.*))">
+ AuthType Basic
+ AuthName "Test Manager"
+ AuthUserFile ${TestManagerRootDir}/misc/htpasswd-sample
+ Require valid-user
+</LocationMatch>
+
+# These two directives are only for local testing!
+Alias /testmanager/htdocs/downloads/VBoxValidationKit.zip ${VBoxBuildOutputDir}/VBoxValidationKit.zip
+<Location /testmanager/htdocs/downloads/VBoxValidationKit.zip>
+ Options Indexes
+ Require all granted
+</Location>
+
+Alias /testmanager/htdocs/ ${TestManagerRootDir}/htdocs/
+<Directory ${TestManagerRootDir}/htdocs/>
+ AllowOverride None
+ Options Indexes
+</Directory>
+
+Alias /testmanager/logs/ /var/tmp/testmanager/
+<Directory /var/tmp/testmanager/>
+ AllowOverride None
+ Options Indexes
+</Directory>
+
+Alias /testmanager/ ${TestManagerRootDir}/cgi/
+<Directory ${TestManagerRootDir}/cgi/>
+ AllowOverride None
+ Options Indexes ExecCGI
+ DirectoryIndex index.py
+ AddHandler cgi-script .py
+</Directory>
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/Makefile.kmk b/src/VBox/ValidationKit/testmanager/batch/Makefile.kmk
new file mode 100644
index 00000000..74d882cc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/Makefile.kmk
@@ -0,0 +1,46 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+SUB_DEPTH = ../../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+
+VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py)
+
+$(evalcall def_vbox_validationkit_process_python_sources)
+$(evalcall def_vbox_validationkit_process_js_sources)
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/add_build.py b/src/VBox/ValidationKit/testmanager/batch/add_build.py
new file mode 100755
index 00000000..a82da5eb
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/add_build.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: add_build.py $
+# pylint: disable=line-too-long
+
+"""
+Interface used by the tinderbox server side software to add a fresh build.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys;
+import os;
+from optparse import OptionParser; # pylint: disable=deprecated-module
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksTestManagerDir);
+
+# Test Manager imports
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.build import BuildDataEx, BuildLogic, BuildCategoryData;
+
+class Build(object): # pylint: disable=too-few-public-methods
+ """
+ Add build info into Test Manager database.
+ """
+
+ def __init__(self):
+ """
+ Parse command line.
+ """
+
+ oParser = OptionParser();
+ oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true',
+ help = 'Quiet execution');
+ oParser.add_option('-b', '--branch', dest = 'sBranch', metavar = '<branch>',
+ help = 'branch name (default: trunk)', default = 'trunk');
+ oParser.add_option('-p', '--product', dest = 'sProductName', metavar = '<name>',
+ help = 'The product name.');
+ oParser.add_option('-r', '--revision', dest = 'iRevision', metavar = '<rev>',
+ help = 'revision number');
+ oParser.add_option('-R', '--repository', dest = 'sRepository', metavar = '<repository>',
+ help = 'Version control repository name.');
+ oParser.add_option('-t', '--type', dest = 'sBuildType', metavar = '<type>',
+ help = 'build type (debug, release etc.)');
+ oParser.add_option('-v', '--version', dest = 'sProductVersion', metavar = '<ver>',
+ help = 'The product version number (suitable for RTStrVersionCompare)');
+ oParser.add_option('-o', '--os-arch', dest = 'asTargetOsArches', metavar = '<os.arch>', action = 'append',
+ help = 'Target OS and architecture. This option can be repeated.');
+ oParser.add_option('-l', '--log', dest = 'sBuildLogPath', metavar = '<url>',
+ help = 'URL to the build logs (optional).');
+ oParser.add_option('-f', '--file', dest = 'asFiles', metavar = '<file|url>', action = 'append',
+ help = 'URLs or build share relative path to a build output file. This option can be repeated.');
+
+ (self.oConfig, _) = oParser.parse_args();
+
+ # Check command line
+ asMissing = [];
+ if self.oConfig.sBranch is None: asMissing.append('--branch');
+ if self.oConfig.iRevision is None: asMissing.append('--revision');
+ if self.oConfig.sProductVersion is None: asMissing.append('--version');
+ if self.oConfig.sProductName is None: asMissing.append('--product');
+ if self.oConfig.sBuildType is None: asMissing.append('--type');
+ if self.oConfig.asTargetOsArches is None: asMissing.append('--os-arch');
+ if self.oConfig.asFiles is None: asMissing.append('--file');
+ if asMissing:
+ sys.stderr.write('syntax error: Missing: %s\n' % (asMissing,));
+ sys.exit(1);
+ # Temporary default.
+ if self.oConfig.sRepository is None:
+ self.oConfig.sRepository = 'vbox';
+
+ def add(self):
+ """
+ Add build data record into database.
+ """
+ oDb = TMDatabaseConnection()
+
+ # Assemble the build data.
+ oBuildData = BuildDataEx()
+ oBuildData.idBuildCategory = None;
+ oBuildData.iRevision = self.oConfig.iRevision
+ oBuildData.sVersion = self.oConfig.sProductVersion
+ oBuildData.sLogUrl = self.oConfig.sBuildLogPath
+ oBuildData.sBinaries = ','.join(self.oConfig.asFiles);
+ oBuildData.oCat = BuildCategoryData().initFromValues(sProduct = self.oConfig.sProductName,
+ sRepository = self.oConfig.sRepository,
+ sBranch = self.oConfig.sBranch,
+ sType = self.oConfig.sBuildType,
+ asOsArches = self.oConfig.asTargetOsArches);
+
+ # Add record to database
+ try:
+ BuildLogic(oDb).addEntry(oBuildData, fCommit = True);
+ except:
+ if self.oConfig.fQuiet:
+ sys.exit(1);
+ raise;
+ oDb.close();
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(Build().add());
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/check_for_deleted_builds.py b/src/VBox/ValidationKit/testmanager/batch/check_for_deleted_builds.py
new file mode 100755
index 00000000..1ac5ab95
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/check_for_deleted_builds.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: check_for_deleted_builds.py $
+# pylint: disable=line-too-long
+
+"""
+Admin job for checking detecting deleted builds.
+
+This is necessary when the tinderbox <-> test manager interface was
+busted and the build info in is out of sync. The result is generally
+a lot of skipped tests because of missing builds, typically during
+bisecting problems.
+"""
+
+from __future__ import print_function;
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys;
+import os;
+from optparse import OptionParser; # pylint: disable=deprecated-module
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksTestManagerDir);
+
+# Test Manager imports
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.build import BuildLogic;
+
+
+
+class BuildChecker(object): # pylint: disable=too-few-public-methods
+ """
+ Add build info into Test Manager database.
+ """
+
+ def __init__(self):
+ """
+ Parse command line.
+ """
+
+ oParser = OptionParser();
+ oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', default = False,
+ help = 'Quiet execution');
+ oParser.add_option('--dry-run', dest = 'fRealRun', action = 'store_false', default = False,
+ help = 'Dry run');
+ oParser.add_option('--real-run', dest = 'fRealRun', action = 'store_true', default = False,
+ help = 'Real run');
+
+ (self.oConfig, _) = oParser.parse_args();
+ if not self.oConfig.fQuiet:
+ if not self.oConfig.fRealRun:
+ print('Dry run.');
+ else:
+ print('Real run! Will commit findings!');
+
+
+ def checkBuilds(self):
+ """
+ Add build data record into database.
+ """
+ oDb = TMDatabaseConnection();
+ oBuildLogic = BuildLogic(oDb);
+
+ tsNow = oDb.getCurrentTimestamp();
+ cMaxRows = 1024;
+ iStart = 0;
+ while True:
+ aoBuilds = oBuildLogic.fetchForListing(iStart, cMaxRows, tsNow);
+ if not self.oConfig.fQuiet and aoBuilds:
+ print('Processing builds #%s thru #%s' % (aoBuilds[0].idBuild, aoBuilds[-1].idBuild));
+
+ for oBuild in aoBuilds:
+ if oBuild.fBinariesDeleted is False:
+ rc = oBuild.areFilesStillThere();
+ if rc is False:
+ if not self.oConfig.fQuiet:
+ print('missing files for build #%s / r%s / %s / %s / %s / %s / %s'
+ % (oBuild.idBuild, oBuild.iRevision, oBuild.sVersion, oBuild.oCat.sType,
+ oBuild.oCat.sBranch, oBuild.oCat.sProduct, oBuild.oCat.asOsArches,));
+ print(' %s' % (oBuild.sBinaries,));
+ if self.oConfig.fRealRun is True:
+ oBuild.fBinariesDeleted = True;
+ oBuildLogic.editEntry(oBuild, fCommit = True);
+ elif rc is True and not self.oConfig.fQuiet:
+ print('build #%s still have its files' % (oBuild.idBuild,));
+ elif rc is None and not self.oConfig.fQuiet:
+ print('Unable to determine state of build #%s' % (oBuild.idBuild,));
+
+ # advance
+ if len(aoBuilds) < cMaxRows:
+ break;
+ iStart += len(aoBuilds);
+
+ oDb.close();
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(BuildChecker().checkBuilds());
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/close_orphaned_testsets.py b/src/VBox/ValidationKit/testmanager/batch/close_orphaned_testsets.py
new file mode 100755
index 00000000..932122b5
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/close_orphaned_testsets.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: close_orphaned_testsets.py $
+# pylint: disable=line-too-long
+
+"""
+Maintenance tool for closing orphaned testsets.
+"""
+
+from __future__ import print_function;
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys
+import os
+from optparse import OptionParser; # pylint: disable=deprecated-module
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+sys.path.append(g_ksTestManagerDir)
+
+# Test Manager imports
+from testmanager.core.db import TMDatabaseConnection
+from testmanager.core.testset import TestSetLogic;
+
+
+class CloseOrphanedTestSets(object):
+ """
+ Finds and closes orphaned testsets.
+ """
+
+ def __init__(self):
+ """
+ Parse command line
+ """
+ oParser = OptionParser();
+ oParser.add_option('-d', '--just-do-it', dest='fJustDoIt', action='store_true',
+ help='Do the database changes.');
+
+
+ (self.oConfig, _) = oParser.parse_args();
+
+
+ def main(self):
+ """ Main method. """
+ oDb = TMDatabaseConnection();
+
+ # Get a list of orphans.
+ oLogic = TestSetLogic(oDb);
+ aoOrphans = oLogic.fetchOrphaned();
+ if aoOrphans:
+ # Complete them.
+ if self.oConfig.fJustDoIt:
+ print('Completing %u test sets as abandoned:' % (len(aoOrphans),));
+ for oTestSet in aoOrphans:
+ print('#%-7u: idTestBox=%-3u tsCreated=%s tsDone=%s'
+ % (oTestSet.idTestSet, oTestSet.idTestBox, oTestSet.tsCreated, oTestSet.tsDone));
+ oLogic.completeAsAbandoned(oTestSet.idTestSet);
+ print('Committing...');
+ oDb.commit();
+ else:
+ for oTestSet in aoOrphans:
+ print('#%-7u: idTestBox=%-3u tsCreated=%s tsDone=%s'
+ % (oTestSet.idTestSet, oTestSet.idTestBox, oTestSet.tsCreated, oTestSet.tsDone));
+ print('Not completing any testsets without seeing the --just-do-it option.');
+ else:
+ print('No orphaned test sets.\n');
+ return 0;
+
+
+if __name__ == '__main__':
+ sys.exit(CloseOrphanedTestSets().main())
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/del_build.py b/src/VBox/ValidationKit/testmanager/batch/del_build.py
new file mode 100755
index 00000000..76e43344
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/del_build.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: del_build.py $
+# pylint: disable=line-too-long
+
+"""
+Interface used by the tinderbox server side software to mark build binaries
+deleted.
+"""
+
+from __future__ import print_function;
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys
+import os
+from optparse import OptionParser; # pylint: disable=deprecated-module
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+sys.path.append(g_ksTestManagerDir)
+
+# Test Manager imports
+from testmanager.core.db import TMDatabaseConnection
+from testmanager.core.build import BuildLogic
+
+
+def markBuildsDeleted():
+ """
+ Marks the builds using the specified binaries as deleted.
+ """
+
+ oParser = OptionParser()
+ oParser.add_option('-q', '--quiet', dest='fQuiet', action='store_true',
+ help='Quiet execution');
+
+ (oConfig, asArgs) = oParser.parse_args()
+ if not asArgs:
+ if not oConfig.fQuiet:
+ sys.stderr.write('syntax error: No builds binaries specified\n');
+ return 1;
+
+
+ oDb = TMDatabaseConnection()
+ oLogic = BuildLogic(oDb)
+
+ for sBuildBin in asArgs:
+ try:
+ cBuilds = oLogic.markDeletedByBinaries(sBuildBin, fCommit = True)
+ except:
+ if oConfig.fQuiet:
+ sys.exit(1);
+ raise;
+ else:
+ if not oConfig.fQuiet:
+ print("del_build.py: Marked %u builds associated with '%s' as deleted." % (cBuilds, sBuildBin,));
+
+ oDb.close()
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(markBuildsDeleted())
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/filearchiver.py b/src/VBox/ValidationKit/testmanager/batch/filearchiver.py
new file mode 100755
index 00000000..10a772ae
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/filearchiver.py
@@ -0,0 +1,282 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: filearchiver.py $
+# pylint: disable=line-too-long
+
+"""
+A cronjob that compresses logs and other files, moving them to the
+g_ksZipFileAreaRootDir storage area.
+"""
+
+from __future__ import print_function;
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys
+import os
+from optparse import OptionParser; # pylint: disable=deprecated-module
+import time;
+import zipfile;
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+sys.path.append(g_ksTestManagerDir)
+
+# Test Manager imports
+from common import utils;
+from testmanager import config;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.testset import TestSetData, TestSetLogic;
+
+
+
+class FileArchiverBatchJob(object): # pylint: disable=too-few-public-methods
+ """
+ Log+files comp
+ """
+
+ def __init__(self, oOptions):
+ """
+ Parse command line
+ """
+ self.fVerbose = oOptions.fVerbose;
+ self.sSrcDir = config.g_ksFileAreaRootDir;
+ self.sDstDir = config.g_ksZipFileAreaRootDir;
+ #self.oTestSetLogic = TestSetLogic(TMDatabaseConnection(self.dprint if self.fVerbose else None));
+ self.oTestSetLogic = TestSetLogic(TMDatabaseConnection(None));
+ self.fDryRun = oOptions.fDryRun;
+
+ def dprint(self, sText):
+ """ Verbose output. """
+ if self.fVerbose:
+ print(sText);
+ return True;
+
+ def warning(self, sText):
+ """Prints a warning."""
+ print(sText);
+ return True;
+
+ def _processTestSet(self, idTestSet, asFiles, sCurDir):
+ """
+ Worker for processDir.
+ Same return codes as processDir.
+ """
+
+ sBaseFilename = os.path.join(sCurDir, 'TestSet-%d' % (idTestSet,));
+ if sBaseFilename[0:2] == ('.' + os.path.sep):
+ sBaseFilename = sBaseFilename[2:];
+ sSrcFileBase = os.path.join(self.sSrcDir, sBaseFilename + '-');
+
+ #
+ # Skip the file if the test set is still running.
+ # But delete them if the testset is not found.
+ #
+ oTestSet = self.oTestSetLogic.tryFetch(idTestSet);
+ if oTestSet is not None and sBaseFilename != oTestSet.sBaseFilename:
+ self.warning('TestSet %d: Deleting because sBaseFilename differs: "%s" (disk) vs "%s" (db)' \
+ % (idTestSet, sBaseFilename, oTestSet.sBaseFilename,));
+ oTestSet = None;
+
+ if oTestSet is not None:
+ if oTestSet.enmStatus == TestSetData.ksTestStatus_Running:
+ self.dprint('Skipping test set #%d, still running' % (idTestSet,));
+ return True;
+
+ #
+ # If we have a zip file already, don't try recreate it as we might
+ # have had trouble removing the source files.
+ #
+ sDstDirPath = os.path.join(self.sDstDir, sCurDir);
+ sZipFileNm = os.path.join(sDstDirPath, 'TestSet-%d.zip' % (idTestSet,));
+ if not os.path.exists(sZipFileNm):
+ #
+ # Create zip file with all testset files as members.
+ #
+ self.dprint('TestSet %d: Creating %s...' % (idTestSet, sZipFileNm,));
+ if not self.fDryRun:
+
+ if not os.path.exists(sDstDirPath):
+ os.makedirs(sDstDirPath, 0o755);
+
+ utils.noxcptDeleteFile(sZipFileNm + '.tmp');
+ with zipfile.ZipFile(sZipFileNm + '.tmp', 'w', zipfile.ZIP_DEFLATED, allowZip64 = True) as oZipFile:
+ for sFile in asFiles:
+ sSuff = os.path.splitext(sFile)[1];
+ if sSuff in [ '.png', '.webm', '.gz', '.bz2', '.zip', '.mov', '.avi', '.mpg', '.gif', '.jpg' ]:
+ ## @todo Consider storing these files outside the zip if they are a little largish.
+ self.dprint('TestSet %d: Storing %s...' % (idTestSet, sFile));
+ oZipFile.write(sSrcFileBase + sFile, sFile, zipfile.ZIP_STORED);
+ else:
+ self.dprint('TestSet %d: Deflating %s...' % (idTestSet, sFile));
+ oZipFile.write(sSrcFileBase + sFile, sFile, zipfile.ZIP_DEFLATED);
+
+ #
+ # .zip.tmp -> .zip.
+ #
+ utils.noxcptDeleteFile(sZipFileNm);
+ os.rename(sZipFileNm + '.tmp', sZipFileNm);
+
+ #else: Dry run.
+ else:
+ self.dprint('TestSet %d: zip file exists already (%s)' % (idTestSet, sZipFileNm,));
+
+ #
+ # Delete the files.
+ #
+ fRc = True;
+ if self.fVerbose:
+ self.dprint('TestSet %d: deleting file: %s' % (idTestSet, asFiles));
+ if not self.fDryRun:
+ for sFile in asFiles:
+ if utils.noxcptDeleteFile(sSrcFileBase + sFile) is False:
+ self.warning('TestSet %d: Failed to delete "%s" (%s)' % (idTestSet, sFile, sSrcFileBase + sFile,));
+ fRc = False;
+
+ return fRc;
+
+
+ def processDir(self, sCurDir):
+ """
+ Process the given directory (relative to sSrcDir and sDstDir).
+ Returns success indicator.
+ """
+ if self.fVerbose:
+ self.dprint('processDir: %s' % (sCurDir,));
+
+ #
+ # Sift thought the directory content, collecting subdirectories and
+ # sort relevant files by test set.
+ # Generally there will either be subdirs or there will be files.
+ #
+ asSubDirs = [];
+ dTestSets = {};
+ sCurPath = os.path.abspath(os.path.join(self.sSrcDir, sCurDir));
+ for sFile in os.listdir(sCurPath):
+ if os.path.isdir(os.path.join(sCurPath, sFile)):
+ if sFile not in [ '.', '..' ]:
+ asSubDirs.append(sFile);
+ elif sFile.startswith('TestSet-'):
+ # Parse the file name. ASSUMES 'TestSet-%d-filename' format.
+ iSlash1 = sFile.find('-');
+ iSlash2 = sFile.find('-', iSlash1 + 1);
+ if iSlash2 <= iSlash1:
+ self.warning('Bad filename (1): "%s"' % (sFile,));
+ continue;
+
+ try: idTestSet = int(sFile[(iSlash1 + 1):iSlash2]);
+ except:
+ self.warning('Bad filename (2): "%s"' % (sFile,));
+ if self.fVerbose:
+ self.dprint('\n'.join(utils.getXcptInfo(4)));
+ continue;
+
+ if idTestSet <= 0:
+ self.warning('Bad filename (3): "%s"' % (sFile,));
+ continue;
+
+ if iSlash2 + 2 >= len(sFile):
+ self.warning('Bad filename (4): "%s"' % (sFile,));
+ continue;
+ sName = sFile[(iSlash2 + 1):];
+
+ # Add it.
+ if idTestSet not in dTestSets:
+ dTestSets[idTestSet] = [];
+ asTestSet = dTestSets[idTestSet];
+ asTestSet.append(sName);
+
+ #
+ # Test sets.
+ #
+ fRc = True;
+ for idTestSet, oTestSet in dTestSets.items():
+ try:
+ if self._processTestSet(idTestSet, oTestSet, sCurDir) is not True:
+ fRc = False;
+ except:
+ self.warning('TestSet %d: Exception in _processTestSet:\n%s' % (idTestSet, '\n'.join(utils.getXcptInfo()),));
+ fRc = False;
+
+ #
+ # Sub dirs.
+ #
+ for sSubDir in asSubDirs:
+ if self.processDir(os.path.join(sCurDir, sSubDir)) is not True:
+ fRc = False;
+
+ #
+ # Try Remove the directory iff it's not '.' and it's been unmodified
+ # for the last 24h (race protection).
+ #
+ if sCurDir != '.':
+ try:
+ fpModTime = float(os.path.getmtime(sCurPath));
+ if fpModTime + (24*3600) <= time.time():
+ if utils.noxcptRmDir(sCurPath) is True:
+ self.dprint('Removed "%s".' % (sCurPath,));
+ except:
+ pass;
+
+ return fRc;
+
+ @staticmethod
+ def main():
+ """ C-style main(). """
+ #
+ # Parse options.
+ #
+ oParser = OptionParser();
+ oParser.add_option('-v', '--verbose', dest = 'fVerbose', action = 'store_true', default = False,
+ help = 'Verbose output.');
+ oParser.add_option('-q', '--quiet', dest = 'fVerbose', action = 'store_false', default = False,
+ help = 'Quiet operation.');
+ oParser.add_option('-d', '--dry-run', dest = 'fDryRun', action = 'store_true', default = False,
+ help = 'Dry run, do not make any changes.');
+ (oOptions, asArgs) = oParser.parse_args()
+ if asArgs != []:
+ oParser.print_help();
+ return 1;
+
+ #
+ # Do the work.
+ #
+ oBatchJob = FileArchiverBatchJob(oOptions);
+ fRc = oBatchJob.processDir('.');
+ return 0 if fRc is True else 1;
+
+if __name__ == '__main__':
+ sys.exit(FileArchiverBatchJob.main());
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/quota.py b/src/VBox/ValidationKit/testmanager/batch/quota.py
new file mode 100755
index 00000000..e2854881
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/quota.py
@@ -0,0 +1,319 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: quota.py $
+# pylint: disable=line-too-long
+
+"""
+A cronjob that applies quotas to large files in testsets.
+"""
+
+from __future__ import print_function;
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys
+import os
+from optparse import OptionParser; # pylint: disable=deprecated-module
+import shutil
+import tempfile;
+import zipfile;
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+sys.path.append(g_ksTestManagerDir)
+
+# Test Manager imports
+from testmanager import config;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.testset import TestSetLogic;
+
+
+class ArchiveDelFilesBatchJob(object): # pylint: disable=too-few-public-methods
+ """
+ Log+files comp
+ """
+
+ def __init__(self, oOptions):
+ """
+ Parse command line
+ """
+ self.fDryRun = oOptions.fDryRun;
+ self.fVerbose = oOptions.fVerbose;
+ self.sTempDir = tempfile.gettempdir();
+
+ self.dprint('Connecting to DB ...');
+ self.oTestSetLogic = TestSetLogic(TMDatabaseConnection(self.dprint if self.fVerbose else None));
+
+ ## Fetches (and handles) all testsets up to this age (in hours).
+ self.uHoursAgeToHandle = 24;
+ ## Always remove files with these extensions.
+ self.asRemoveFileExt = [ 'webm' ];
+ ## Always remove files which are bigger than this limit.
+ # Set to 0 to disable.
+ self.cbRemoveBiggerThan = 128 * 1024 * 1024;
+
+ def dprint(self, sText):
+ """ Verbose output. """
+ if self.fVerbose:
+ print(sText);
+ return True;
+
+ def warning(self, sText):
+ """Prints a warning."""
+ print(sText);
+ return True;
+
+ def _replaceFile(self, sDstFile, sSrcFile, fDryRun = False, fForce = False):
+ """
+ Replaces / moves a file safely by backing up the existing destination file (if any).
+
+ Returns success indicator.
+ """
+
+ fRc = True;
+
+ # Rename the destination file first (if any).
+ sDstFileTmp = None;
+ if os.path.exists(sDstFile):
+ sDstFileTmp = sDstFile + ".bak";
+ if os.path.exists(sDstFileTmp):
+ if not fForce:
+ print('Replace file: Warning: Temporary destination file "%s" already exists, skipping' % (sDstFileTmp,));
+ fRc = False;
+ else:
+ try:
+ os.remove(sDstFileTmp);
+ except Exception as e:
+ print('Replace file: Error deleting old temporary destination file "%s": %s' % (sDstFileTmp, e));
+ fRc = False;
+ try:
+ if not fDryRun:
+ shutil.move(sDstFile, sDstFileTmp);
+ except Exception as e:
+ print('Replace file: Error moving old destination file "%s" to temporary file "%s": %s' \
+ % (sDstFile, sDstFileTmp, e));
+ fRc = False;
+
+ if not fRc:
+ return False;
+
+ try:
+ if not fDryRun:
+ shutil.move(sSrcFile, sDstFile);
+ except Exception as e:
+ print('Replace file: Error moving source file "%s" to destination "%s": %s' % (sSrcFile, sDstFile, e,));
+ fRc = False;
+
+ if sDstFileTmp:
+ if fRc: # Move succeeded, remove backup.
+ try:
+ if not fDryRun:
+ os.remove(sDstFileTmp);
+ except Exception as e:
+ print('Replace file: Error deleting temporary destination file "%s": %s' % (sDstFileTmp, e));
+ fRc = False;
+ else: # Final move failed, roll back.
+ try:
+ if not fDryRun:
+ shutil.move(sDstFileTmp, sDstFile);
+ except Exception as e:
+ print('Replace file: Error restoring old destination file "%s": %s' % (sDstFile, e));
+ fRc = False;
+ return fRc;
+
+ def _processTestSetZip(self, idTestSet, sSrcZipFileAbs):
+ """
+ Worker for processOneTestSet, which processes the testset's ZIP file.
+
+ Returns success indicator.
+ """
+ _ = idTestSet
+
+ with tempfile.NamedTemporaryFile(dir=self.sTempDir, delete=False) as tmpfile:
+ sDstZipFileAbs = tmpfile.name;
+
+ fRc = True;
+
+ try:
+ oSrcZipFile = zipfile.ZipFile(sSrcZipFileAbs, 'r'); # pylint: disable=consider-using-with
+ self.dprint('Processing ZIP archive "%s" ...' % (sSrcZipFileAbs));
+ try:
+ if not self.fDryRun:
+ oDstZipFile = zipfile.ZipFile(sDstZipFileAbs, 'w'); # pylint: disable=consider-using-with
+ self.dprint('Using temporary ZIP archive "%s"' % (sDstZipFileAbs));
+ try:
+ #
+ # First pass: Gather information if we need to do some re-packing.
+ #
+ fDoRepack = False;
+ aoFilesToRepack = [];
+ for oCurFile in oSrcZipFile.infolist():
+ self.dprint('Handling File "%s" ...' % (oCurFile.filename))
+ sFileExt = os.path.splitext(oCurFile.filename)[1];
+
+ if sFileExt \
+ and sFileExt[1:] in self.asRemoveFileExt:
+ self.dprint('\tMatches excluded extensions')
+ fDoRepack = True;
+ elif self.cbRemoveBiggerThan \
+ and oCurFile.file_size > self.cbRemoveBiggerThan:
+ self.dprint('\tIs bigger than %d bytes (%d bytes)' % (self.cbRemoveBiggerThan, oCurFile.file_size))
+ fDoRepack = True;
+ else:
+ aoFilesToRepack.append(oCurFile);
+
+ if not fDoRepack:
+ oSrcZipFile.close();
+ self.dprint('No re-packing necessary, skipping ZIP archive');
+ return True;
+
+ #
+ # Second pass: Re-pack all needed files into our temporary ZIP archive.
+ #
+ for oCurFile in aoFilesToRepack:
+ self.dprint('Re-packing file "%s"' % (oCurFile.filename,))
+ if not self.fDryRun:
+ oBuf = oSrcZipFile.read(oCurFile);
+ oDstZipFile.writestr(oCurFile, oBuf);
+
+ if not self.fDryRun:
+ oDstZipFile.close();
+
+ except Exception as oXcpt4:
+ print('Error handling file "%s" of archive "%s": %s' % (oCurFile.filename, sSrcZipFileAbs, oXcpt4,));
+ return False;
+
+ oSrcZipFile.close();
+
+ if fRc:
+ self.dprint('Moving file "%s" to "%s"' % (sDstZipFileAbs, sSrcZipFileAbs));
+ fRc = self._replaceFile(sSrcZipFileAbs, sDstZipFileAbs, self.fDryRun);
+
+ except Exception as oXcpt3:
+ print('Error creating temporary ZIP archive "%s": %s' % (sDstZipFileAbs, oXcpt3,));
+ return False;
+
+ except Exception as oXcpt1:
+ # Construct a meaningful error message.
+ if os.path.exists(sSrcZipFileAbs):
+ print('Error: Opening file "%s" failed: %s' % (sSrcZipFileAbs, oXcpt1));
+ else:
+ print('Error: File "%s" not found.' % (sSrcZipFileAbs,));
+ return False;
+
+ return fRc;
+
+
+ def processOneTestSet(self, idTestSet, sBasename):
+ """
+ Processes one single testset.
+
+ Returns success indicator.
+ """
+
+ fRc = True;
+ self.dprint('Processing testset %d' % (idTestSet,));
+
+ # Construct absolute ZIP file path.
+ # ZIP is hardcoded in config, so do here.
+ sSrcZipFileAbs = os.path.join(config.g_ksZipFileAreaRootDir, sBasename + '.zip');
+
+ if self._processTestSetZip(idTestSet, sSrcZipFileAbs) is not True:
+ fRc = False;
+
+ return fRc;
+
+ def processTestSets(self):
+ """
+ Processes all testsets according to the set configuration.
+
+ Returns success indicator.
+ """
+
+ aoTestSets = self.oTestSetLogic.fetchByAge(cHoursBack = self.uHoursAgeToHandle);
+ cTestSets = len(aoTestSets);
+ print('Found %d entries in DB' % cTestSets);
+ if not cTestSets:
+ return True; # Nothing to do (yet).
+
+ fRc = True;
+ for oTestSet in aoTestSets:
+ fRc = self.processOneTestSet(oTestSet.idTestSet, oTestSet.sBaseFilename) and fRc;
+ # Keep going.
+
+ return fRc;
+
+ @staticmethod
+ def main():
+ """ C-style main(). """
+ #
+ # Parse options.
+ #
+
+ oParser = OptionParser();
+
+ # Generic options.
+ oParser.add_option('-v', '--verbose', dest = 'fVerbose', action = 'store_true', default = False,
+ help = 'Verbose output.');
+ oParser.add_option('-q', '--quiet', dest = 'fVerbose', action = 'store_false', default = False,
+ help = 'Quiet operation.');
+ oParser.add_option('-d', '--dry-run', dest = 'fDryRun', action = 'store_true', default = False,
+ help = 'Dry run, do not make any changes.');
+
+ (oOptions, asArgs) = oParser.parse_args(sys.argv[1:]);
+ if asArgs != []:
+ oParser.print_help();
+ return 1;
+
+ if oOptions.fDryRun:
+ print('***********************************');
+ print('*** DRY RUN - NO FILES MODIFIED ***');
+ print('***********************************');
+
+ #
+ # Do the work.
+ #
+ fRc = False;
+
+ oBatchJob = ArchiveDelFilesBatchJob(oOptions);
+ fRc = oBatchJob.processTestSets();
+
+ if oOptions.fVerbose:
+ print('SUCCESS' if fRc else 'FAILURE');
+
+ return 0 if fRc is True else 1;
+
+if __name__ == '__main__':
+ sys.exit(ArchiveDelFilesBatchJob.main());
diff --git a/src/VBox/ValidationKit/testmanager/batch/regen_sched_queues.py b/src/VBox/ValidationKit/testmanager/batch/regen_sched_queues.py
new file mode 100755
index 00000000..a20d6cbe
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/regen_sched_queues.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: regen_sched_queues.py $
+# pylint: disable=line-too-long
+
+"""
+Interface used by the admin to regenerate scheduling queues.
+"""
+
+from __future__ import print_function;
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys;
+import os;
+from optparse import OptionParser; # pylint: disable=deprecated-module
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksTestManagerDir);
+
+# Test Manager imports
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.schedulerbase import SchedulerBase;
+from testmanager.core.schedgroup import SchedGroupLogic;
+
+
+
+class RegenSchedQueues(object): # pylint: disable=too-few-public-methods
+ """
+ Regenerates all the scheduling queues.
+ """
+
+ def __init__(self):
+ """
+ Parse command line.
+ """
+
+ oParser = OptionParser();
+ oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', default = False,
+ help = 'Quiet execution');
+ oParser.add_option('-u', '--uid', dest = 'uid', action = 'store', type = 'int', default = 1,
+ help = 'User ID to accredit with this job');
+ oParser.add_option('--profile', dest = 'fProfile', action = 'store_true', default = False,
+ help = 'User ID to accredit with this job');
+
+ (self.oConfig, _) = oParser.parse_args();
+
+
+ def doIt(self):
+ """
+ Does the job.
+ """
+ oDb = TMDatabaseConnection();
+
+ aoGroups = SchedGroupLogic(oDb).getAll();
+ iRc = 0;
+ for oGroup in aoGroups:
+ if not self.oConfig.fQuiet:
+ print('%s (ID %#d):' % (oGroup.sName, oGroup.idSchedGroup,));
+ try:
+ (aoErrors, asMessages) = SchedulerBase.recreateQueue(oDb, self.oConfig.uid, oGroup.idSchedGroup, 2);
+ except Exception as oXcpt:
+ oDb.rollback();
+ print(' !!Hit exception processing "%s": %s' % (oGroup.sName, oXcpt,));
+ else:
+ if not aoErrors:
+ if not self.oConfig.fQuiet:
+ print(' Successfully regenerated.');
+ else:
+ iRc = 1;
+ print(' %d errors:' % (len(aoErrors,)));
+ for oError in aoErrors:
+ if oError[1] is None:
+ print(' !!%s' % (oError[0],));
+ else:
+ print(' !!%s (%s)' % (oError[0], oError[1]));
+ if asMessages and not self.oConfig.fQuiet:
+ print(' %d messages:' % (len(asMessages),));
+ for sMsg in asMessages:
+ print(' ##%s' % (sMsg,));
+ return iRc;
+
+ @staticmethod
+ def main():
+ """ Main function. """
+ oMain = RegenSchedQueues();
+ if oMain.oConfig.fProfile is not True:
+ iRc = oMain.doIt();
+ else:
+ import cProfile;
+ oProfiler = cProfile.Profile();
+ iRc = oProfiler.runcall(oMain.doIt);
+ oProfiler.print_stats(sort = 'time');
+ oProfiler = None;
+ return iRc;
+
+if __name__ == '__main__':
+ sys.exit(RegenSchedQueues().main());
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/vcs_import.py b/src/VBox/ValidationKit/testmanager/batch/vcs_import.py
new file mode 100755
index 00000000..bffa576b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/vcs_import.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: vcs_import.py $
+
+"""
+Cron job for importing revision history for a repository.
+"""
+
+from __future__ import print_function;
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys;
+import os;
+from optparse import OptionParser; # pylint: disable=deprecated-module
+import xml.etree.ElementTree as ET;
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksTestManagerDir);
+
+# Test Manager imports
+from testmanager.config import g_kdBugTrackers;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.vcsrevisions import VcsRevisionData, VcsRevisionLogic;
+from testmanager.core.vcsbugreference import VcsBugReferenceData, VcsBugReferenceLogic;
+from common import utils;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ long = int; # pylint: disable=redefined-builtin,invalid-name
+
+
+class VcsImport(object): # pylint: disable=too-few-public-methods
+ """
+ Imports revision history from a VSC into the Test Manager database.
+ """
+
+ class BugTracker(object):
+ def __init__(self, sDbName, sTag):
+ self.sDbName = sDbName;
+ self.sTag = sTag;
+
+
+ def __init__(self):
+ """
+ Parse command line.
+ """
+
+ oParser = OptionParser()
+ oParser.add_option('-b', '--only-bug-refs', dest = 'fBugRefsOnly', action = 'store_true',
+ help = 'Only do bug references, not revisions.');
+ oParser.add_option('-e', '--extra-option', dest = 'asExtraOptions', metavar = 'vcsoption', action = 'append',
+ help = 'Adds a extra option to the command retrieving the log.');
+ oParser.add_option('-f', '--full', dest = 'fFull', action = 'store_true',
+ help = 'Full revision history import.');
+ oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true',
+ help = 'Quiet execution');
+ oParser.add_option('-R', '--repository', dest = 'sRepository', metavar = '<repository>',
+ help = 'Version control repository name.');
+ oParser.add_option('-s', '--start-revision', dest = 'iStartRevision', metavar = 'start-revision',
+ type = "int", default = 0,
+ help = 'The revision to start at when doing a full import.');
+ oParser.add_option('-t', '--type', dest = 'sType', metavar = '<type>',
+ help = 'The VCS type (default: svn)', choices = [ 'svn', ], default = 'svn');
+ oParser.add_option('-u', '--url', dest = 'sUrl', metavar = '<url>',
+ help = 'The VCS URL');
+
+ (self.oConfig, _) = oParser.parse_args();
+
+ # Check command line
+ asMissing = [];
+ if self.oConfig.sUrl is None: asMissing.append('--url');
+ if self.oConfig.sRepository is None: asMissing.append('--repository');
+ if asMissing:
+ sys.stderr.write('syntax error: Missing: %s\n' % (asMissing,));
+ sys.exit(1);
+
+ assert self.oConfig.sType == 'svn';
+
+ def main(self):
+ """
+ Main function.
+ """
+ oDb = TMDatabaseConnection();
+ oLogic = VcsRevisionLogic(oDb);
+ oBugLogic = VcsBugReferenceLogic(oDb);
+
+ # Where to start.
+ iStartRev = 0;
+ if not self.oConfig.fFull:
+ if not self.oConfig.fBugRefsOnly:
+ iStartRev = oLogic.getLastRevision(self.oConfig.sRepository);
+ else:
+ iStartRev = oBugLogic.getLastRevision(self.oConfig.sRepository);
+ if iStartRev == 0:
+ iStartRev = self.oConfig.iStartRevision;
+
+ # Construct a command line.
+ os.environ['LC_ALL'] = 'en_US.utf-8';
+ asArgs = [
+ 'svn',
+ 'log',
+ '--xml',
+ '--revision', str(iStartRev) + ':HEAD',
+ ];
+ if self.oConfig.asExtraOptions is not None:
+ asArgs.extend(self.oConfig.asExtraOptions);
+ asArgs.append(self.oConfig.sUrl);
+ if not self.oConfig.fQuiet:
+ print('Executing: %s' % (asArgs,));
+ sLogXml = utils.processOutputChecked(asArgs);
+
+ # Parse the XML and add the entries to the database.
+ oParser = ET.XMLParser(target = ET.TreeBuilder(), encoding = 'utf-8');
+ oParser.feed(sLogXml.encode('utf-8')); # Does its own decoding; processOutputChecked always gives us decoded utf-8 now.
+ oRoot = oParser.close();
+
+ for oLogEntry in oRoot.findall('logentry'):
+ iRevision = int(oLogEntry.get('revision'));
+ sAuthor = oLogEntry.findtext('author', 'unspecified').strip(); # cvs2svn entries doesn't have an author.
+ sDate = oLogEntry.findtext('date').strip();
+ sRawMsg = oLogEntry.findtext('msg', '').strip();
+ sMessage = sRawMsg;
+ if sMessage == '':
+ sMessage = ' ';
+ elif len(sMessage) > VcsRevisionData.kcchMax_sMessage:
+ sMessage = sMessage[:VcsRevisionData.kcchMax_sMessage - 4] + ' ...';
+ if not self.oConfig.fQuiet:
+ utils.printOut(u'sDate=%s iRev=%u sAuthor=%s sMsg[%s]=%s'
+ % (sDate, iRevision, sAuthor, type(sMessage).__name__, sMessage));
+
+ if not self.oConfig.fBugRefsOnly:
+ oData = VcsRevisionData().initFromValues(self.oConfig.sRepository, iRevision, sDate, sAuthor, sMessage);
+ oLogic.addVcsRevision(oData);
+
+ # Analyze the raw message looking for bug tracker references.
+ for oBugTracker in g_kdBugTrackers.values():
+ for sTag in oBugTracker.asCommitTags:
+ off = sRawMsg.find(sTag);
+ while off >= 0:
+ off += len(sTag);
+ while off < len(sRawMsg) and sRawMsg[off].isspace():
+ off += 1;
+
+ if off < len(sRawMsg) and sRawMsg[off].isdigit():
+ offNum = off;
+ while off < len(sRawMsg) and sRawMsg[off].isdigit():
+ off += 1;
+ try:
+ iBugNo = long(sRawMsg[offNum:off]);
+ except Exception as oXcpt:
+ utils.printErr(u'error! exception(r%s,"%s"): -> %s' % (iRevision, sRawMsg[offNum:off], oXcpt,));
+ else:
+ if not self.oConfig.fQuiet:
+ utils.printOut(u' r%u -> sBugTracker=%s iBugNo=%s'
+ % (iRevision, oBugTracker.sDbId, iBugNo,));
+
+ oBugData = VcsBugReferenceData().initFromValues(self.oConfig.sRepository, iRevision,
+ oBugTracker.sDbId, iBugNo);
+ oBugLogic.addVcsBugReference(oBugData);
+
+ # next
+ off = sRawMsg.find(sTag, off);
+
+ oDb.commit();
+
+ oDb.close();
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(VcsImport().main());
+
diff --git a/src/VBox/ValidationKit/testmanager/batch/virtual_test_sheriff.py b/src/VBox/ValidationKit/testmanager/batch/virtual_test_sheriff.py
new file mode 100755
index 00000000..51999e21
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/batch/virtual_test_sheriff.py
@@ -0,0 +1,1832 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: virtual_test_sheriff.py $
+# pylint: disable=line-too-long
+
+"""
+Virtual Test Sheriff.
+
+Duties:
+ - Try to a assign failure reasons to recently failed tests.
+ - Reboot or disable bad test boxes.
+
+"""
+
+from __future__ import print_function;
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports
+import hashlib;
+import os;
+import re;
+import smtplib;
+#import subprocess;
+import sys;
+from email.mime.multipart import MIMEMultipart;
+from email.mime.text import MIMEText;
+from email.utils import COMMASPACE;
+
+if sys.version_info[0] >= 3:
+ from io import BytesIO as BytesIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias
+else:
+ from StringIO import StringIO as BytesIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias
+from optparse import OptionParser; # pylint: disable=deprecated-module
+from PIL import Image; # pylint: disable=import-error
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksTestManagerDir);
+
+# Test Manager imports
+from common import utils;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.build import BuildDataEx;
+from testmanager.core.failurereason import FailureReasonLogic;
+from testmanager.core.testbox import TestBoxLogic, TestBoxData;
+from testmanager.core.testcase import TestCaseDataEx;
+from testmanager.core.testgroup import TestGroupData;
+from testmanager.core.testset import TestSetLogic, TestSetData;
+from testmanager.core.testresults import TestResultLogic, TestResultFileData;
+from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData;
+from testmanager.core.useraccount import UserAccountLogic;
+from testmanager.config import g_ksSmtpHost, g_kcSmtpPort, g_ksAlertFrom, \
+ g_ksAlertSubject, g_asAlertList #, g_ksLomPassword;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ xrange = range; # pylint: disable=redefined-builtin,invalid-name
+
+
+class VirtualTestSheriffCaseFile(object):
+ """
+ A failure investigation case file.
+
+ """
+
+
+ ## Max log file we'll read into memory. (256 MB)
+ kcbMaxLogRead = 0x10000000;
+
+ def __init__(self, oSheriff, oTestSet, oTree, oBuild, oTestBox, oTestGroup, oTestCase):
+ self.oSheriff = oSheriff;
+ self.oTestSet = oTestSet; # TestSetData
+ self.oTree = oTree; # TestResultDataEx
+ self.oBuild = oBuild; # BuildDataEx
+ self.oTestBox = oTestBox; # TestBoxData
+ self.oTestGroup = oTestGroup; # TestGroupData
+ self.oTestCase = oTestCase; # TestCaseDataEx
+ self.sMainLog = ''; # The main log file. Empty string if not accessible.
+ self.sSvcLog = ''; # The VBoxSVC log file. Empty string if not accessible.
+
+ # Generate a case file name.
+ self.sName = '#%u: %s' % (self.oTestSet.idTestSet, self.oTestCase.sName,)
+ self.sLongName = '#%u: "%s" on "%s" running %s %s (%s), "%s" by %s, using %s %s %s r%u' \
+ % ( self.oTestSet.idTestSet,
+ self.oTestCase.sName,
+ self.oTestBox.sName,
+ self.oTestBox.sOs,
+ self.oTestBox.sOsVersion,
+ self.oTestBox.sCpuArch,
+ self.oTestBox.sCpuName,
+ self.oTestBox.sCpuVendor,
+ self.oBuild.oCat.sProduct,
+ self.oBuild.oCat.sBranch,
+ self.oBuild.oCat.sType,
+ self.oBuild.iRevision, );
+
+ # Investigation notes.
+ self.tReason = None; # None or one of the ktReason_XXX constants.
+ self.dReasonForResultId = {}; # Reason assignments indexed by idTestResult.
+ self.dCommentForResultId = {}; # Comment assignments indexed by idTestResult.
+
+ #
+ # Reason.
+ #
+
+ def noteReason(self, tReason):
+ """ Notes down a possible reason. """
+ self.oSheriff.dprint(u'noteReason: %s -> %s' % (self.tReason, tReason,));
+ self.tReason = tReason;
+ return True;
+
+ def noteReasonForId(self, tReason, idTestResult, sComment = None):
+ """ Notes down a possible reason for a specific test result. """
+ self.oSheriff.dprint(u'noteReasonForId: %u: %s -> %s%s'
+ % (idTestResult, self.dReasonForResultId.get(idTestResult, None), tReason,
+ (u' (%s)' % (sComment,)) if sComment is not None else ''));
+ self.dReasonForResultId[idTestResult] = tReason;
+ if sComment is not None:
+ self.dCommentForResultId[idTestResult] = sComment;
+ return True;
+
+
+ #
+ # Test classification.
+ #
+
+ def isVBoxTest(self):
+ """ Test classification: VirtualBox (using the build) """
+ return self.oBuild.oCat.sProduct.lower() in [ 'virtualbox', 'vbox' ];
+
+ def isVBoxUnitTest(self):
+ """ Test case classification: The unit test doing all our testcase/*.cpp stuff. """
+ return self.isVBoxTest() \
+ and (self.oTestCase.sName.lower() == 'unit tests' or self.oTestCase.sName.lower().startswith('misc: unit tests'));
+
+ def isVBoxInstallTest(self):
+ """ Test case classification: VirtualBox Guest installation test. """
+ return self.isVBoxTest() \
+ and self.oTestCase.sName.lower().startswith('install:');
+
+ def isVBoxUnattendedInstallTest(self):
+ """ Test case classification: VirtualBox Guest installation test. """
+ return self.isVBoxTest() \
+ and self.oTestCase.sName.lower().startswith('uinstall:');
+
+ def isVBoxUSBTest(self):
+ """ Test case classification: VirtualBox USB test. """
+ return self.isVBoxTest() \
+ and self.oTestCase.sName.lower().startswith('usb:');
+
+ def isVBoxStorageTest(self):
+ """ Test case classification: VirtualBox Storage test. """
+ return self.isVBoxTest() \
+ and self.oTestCase.sName.lower().startswith('storage:');
+
+ def isVBoxGAsTest(self):
+ """ Test case classification: VirtualBox Guest Additions test. """
+ return self.isVBoxTest() \
+ and ( self.oTestCase.sName.lower().startswith('guest additions')
+ or self.oTestCase.sName.lower().startswith('ga\'s tests'));
+
+ def isVBoxAPITest(self):
+ """ Test case classification: VirtualBox API test. """
+ return self.isVBoxTest() \
+ and self.oTestCase.sName.lower().startswith('api:');
+
+ def isVBoxBenchmarkTest(self):
+ """ Test case classification: VirtualBox Benchmark test. """
+ return self.isVBoxTest() \
+ and self.oTestCase.sName.lower().startswith('benchmark:');
+
+ def isVBoxSmokeTest(self):
+ """ Test case classification: Smoke test. """
+ return self.isVBoxTest() \
+ and self.oTestCase.sName.lower().startswith('smoketest');
+
+ def isVBoxSerialTest(self):
+ """ Test case classification: Smoke test. """
+ return self.isVBoxTest() \
+ and self.oTestCase.sName.lower().startswith('serial:');
+
+
+ #
+ # Utility methods.
+ #
+
+ def getMainLog(self):
+ """
+ Tries to read the main log file since this will be the first source of information.
+ """
+ if self.sMainLog:
+ return self.sMainLog;
+ (oFile, oSizeOrError, _) = self.oTestSet.openFile('main.log', 'rb');
+ if oFile is not None:
+ try:
+ self.sMainLog = oFile.read(min(self.kcbMaxLogRead, oSizeOrError)).decode('utf-8', 'replace');
+ except Exception as oXcpt:
+ self.oSheriff.vprint(u'Error reading main log file: %s' % (oXcpt,))
+ self.sMainLog = '';
+ else:
+ self.oSheriff.vprint(u'Error opening main log file: %s' % (oSizeOrError,));
+ return self.sMainLog;
+
+ def getLogFile(self, oFile):
+ """
+ Tries to read the given file as a utf-8 log file.
+ oFile is a TestFileDataEx instance.
+ Returns empty string if problems opening or reading the file.
+ """
+ sContent = '';
+ (oFile, oSizeOrError, _) = self.oTestSet.openFile(oFile.sFile, 'rb');
+ if oFile is not None:
+ try:
+ sContent = oFile.read(min(self.kcbMaxLogRead, oSizeOrError)).decode('utf-8', 'replace');
+ except Exception as oXcpt:
+ self.oSheriff.vprint(u'Error reading the "%s" log file: %s' % (oFile.sFile, oXcpt,))
+ else:
+ self.oSheriff.vprint(u'Error opening the "%s" log file: %s' % (oFile.sFile, oSizeOrError,));
+ return sContent;
+
+ def getSvcLog(self):
+ """
+ Tries to read the VBoxSVC log file as it typically not associated with a failing test result.
+ Note! Returns the first VBoxSVC log file we find.
+ """
+ if not self.sSvcLog:
+ aoSvcLogFiles = self.oTree.getListOfLogFilesByKind(TestResultFileData.ksKind_LogReleaseSvc);
+ if aoSvcLogFiles:
+ self.sSvcLog = self.getLogFile(aoSvcLogFiles[0]);
+ return self.sSvcLog;
+
+ def getScreenshotSha256(self, oFile):
+ """
+ Tries to read the given screenshot file, uncompress it, and do SHA-2
+ on the raw pixels.
+ Returns SHA-2 digest string on success, None on failure.
+ """
+ (oImgFile, _, _) = self.oTestSet.openFile(oFile.sFile, 'rb');
+ try:
+ abImageFile = oImgFile.read();
+ except Exception as oXcpt:
+ self.oSheriff.vprint(u'Error reading the "%s" image file: %s' % (oFile.sFile, oXcpt,))
+ else:
+ try:
+ oImage = Image.open(BytesIO(abImageFile));
+ except Exception as oXcpt:
+ self.oSheriff.vprint(u'Error opening the "%s" image bytes using PIL.Image.open: %s' % (oFile.sFile, oXcpt,))
+ else:
+ try:
+ oHash = hashlib.sha256();
+ if hasattr(oImage, 'tobytes'):
+ oHash.update(oImage.tobytes());
+ else:
+ oHash.update(oImage.tostring()); # pylint: disable=no-member
+ except Exception as oXcpt:
+ self.oSheriff.vprint(u'Error hashing the uncompressed image bytes for "%s": %s' % (oFile.sFile, oXcpt,))
+ else:
+ return oHash.hexdigest();
+ return None;
+
+
+
+ def isSingleTestFailure(self):
+ """
+ Figure out if this is a single test failing or if it's one of the
+ more complicated ones.
+ """
+ if self.oTree.cErrors == 1:
+ return True;
+ if self.oTree.deepCountErrorContributers() <= 1:
+ return True;
+ return False;
+
+
+
+class VirtualTestSheriff(object): # pylint: disable=too-few-public-methods
+ """
+ Add build info into Test Manager database.
+ """
+
+ ## The user account for the virtual sheriff.
+ ksLoginName = 'vsheriff';
+
+ def __init__(self):
+ """
+ Parse command line.
+ """
+ self.oDb = None;
+ self.tsNow = None;
+ self.oTestResultLogic = None;
+ self.oTestSetLogic = None;
+ self.oFailureReasonLogic = None; # FailureReasonLogic;
+ self.oTestResultFailureLogic = None; # TestResultFailureLogic
+ self.oLogin = None;
+ self.uidSelf = -1;
+ self.oLogFile = None;
+ self.asBsodReasons = [];
+ self.asUnitTestReasons = [];
+
+ oParser = OptionParser();
+ oParser.add_option('--start-hours-ago', dest = 'cStartHoursAgo', metavar = '<hours>', default = 0, type = 'int',
+ help = 'When to start specified as hours relative to current time. Defauls is right now.', );
+ oParser.add_option('--hours-period', dest = 'cHoursBack', metavar = '<period-in-hours>', default = 2, type = 'int',
+ help = 'Work period specified in hours. Defauls is 2 hours.');
+ oParser.add_option('--real-run-back', dest = 'fRealRun', action = 'store_true', default = False,
+ help = 'Whether to commit the findings to the database. Default is a dry run.');
+ oParser.add_option('--testset', dest = 'aidTestSets', metavar = '<id>', default = [], type = 'int', action = 'append',
+ help = 'Only investigate this one. Accumulates IDs when repeated.');
+ oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', default = False,
+ help = 'Quiet execution');
+ oParser.add_option('-l', '--log', dest = 'sLogFile', metavar = '<logfile>', default = None,
+ help = 'Where to log messages.');
+ oParser.add_option('--debug', dest = 'fDebug', action = 'store_true', default = False,
+ help = 'Enables debug mode.');
+
+ (self.oConfig, _) = oParser.parse_args();
+
+ if self.oConfig.sLogFile:
+ self.oLogFile = open(self.oConfig.sLogFile, "a"); # pylint: disable=consider-using-with,unspecified-encoding
+ self.oLogFile.write('VirtualTestSheriff: $Revision: 155244 $ \n');
+
+
+ def eprint(self, sText):
+ """
+ Prints error messages.
+ Returns 1 (for exit code usage.)
+ """
+ print('error: %s' % (sText,));
+ if self.oLogFile is not None:
+ if sys.version_info[0] >= 3:
+ self.oLogFile.write(u'error: %s\n' % (sText,));
+ else:
+ self.oLogFile.write((u'error: %s\n' % (sText,)).encode('utf-8'));
+ return 1;
+
+ def dprint(self, sText):
+ """
+ Prints debug info.
+ """
+ if self.oConfig.fDebug:
+ if not self.oConfig.fQuiet:
+ print('debug: %s' % (sText, ));
+ if self.oLogFile is not None:
+ if sys.version_info[0] >= 3:
+ self.oLogFile.write(u'debug: %s\n' % (sText,));
+ else:
+ self.oLogFile.write((u'debug: %s\n' % (sText,)).encode('utf-8'));
+ return 0;
+
+ def vprint(self, sText):
+ """
+ Prints verbose info.
+ """
+ if not self.oConfig.fQuiet:
+ print('info: %s' % (sText,));
+ if self.oLogFile is not None:
+ if sys.version_info[0] >= 3:
+ self.oLogFile.write(u'info: %s\n' % (sText,));
+ else:
+ self.oLogFile.write((u'info: %s\n' % (sText,)).encode('utf-8'));
+ return 0;
+
+ def getFailureReason(self, tReason):
+ """ Gets the failure reason object for tReason. """
+ return self.oFailureReasonLogic.cachedLookupByNameAndCategory(tReason[1], tReason[0]);
+
+ def selfCheck(self):
+ """ Does some self checks, looking up things we expect to be in the database and such. """
+ rcExit = 0;
+ for sAttr in dir(self.__class__):
+ if sAttr.startswith('ktReason_'):
+ tReason = getattr(self.__class__, sAttr);
+ oFailureReason = self.getFailureReason(tReason);
+ if oFailureReason is None:
+ rcExit = self.eprint(u'Failed to find failure reason "%s" in category "%s" in the database!'
+ % (tReason[1], tReason[0],));
+
+ # Check the user account as well.
+ if self.oLogin is None:
+ oLogin = UserAccountLogic(self.oDb).tryFetchAccountByLoginName(VirtualTestSheriff.ksLoginName);
+ if oLogin is None:
+ rcExit = self.eprint(u'Cannot find my user account "%s"!' % (VirtualTestSheriff.ksLoginName,));
+ return rcExit;
+
+ def sendEmailAlert(self, uidAuthor, sBodyText):
+ """
+ Sends email alert.
+ """
+
+ # Get author email
+ self.oDb.execute('SELECT sEmail FROM Users WHERE uid=%s', (uidAuthor,));
+ sFrom = self.oDb.fetchOne();
+ if sFrom is not None:
+ sFrom = sFrom[0];
+ else:
+ sFrom = g_ksAlertFrom;
+
+ # Gather recipient list.
+ asEmailList = [];
+ for sUser in g_asAlertList:
+ self.oDb.execute('SELECT sEmail FROM Users WHERE sUsername=%s', (sUser,));
+ sEmail = self.oDb.fetchOne();
+ if sEmail:
+ asEmailList.append(sEmail[0]);
+ if not asEmailList:
+ return self.eprint('No email addresses to send alter to!');
+
+ # Compose the message.
+ oMsg = MIMEMultipart();
+ oMsg['From'] = sFrom;
+ oMsg['To'] = COMMASPACE.join(asEmailList);
+ oMsg['Subject'] = g_ksAlertSubject;
+ oMsg.attach(MIMEText(sBodyText, 'plain'))
+
+ # Try send it.
+ try:
+ oSMTP = smtplib.SMTP(g_ksSmtpHost, g_kcSmtpPort);
+ oSMTP.sendmail(sFrom, asEmailList, oMsg.as_string())
+ oSMTP.quit()
+ except smtplib.SMTPException as oXcpt:
+ return self.eprint('Failed to send mail: %s' % (oXcpt,));
+
+ return 0;
+
+ def badTestBoxManagement(self):
+ """
+ Looks for bad test boxes and first tries once to reboot them then disables them.
+ """
+ rcExit = 0;
+
+ #
+ # We skip this entirely if we're running in the past and not in harmless debug mode.
+ #
+ if self.oConfig.cStartHoursAgo != 0 \
+ and (not self.oConfig.fDebug or self.oConfig.fRealRun):
+ return rcExit;
+ tsNow = self.tsNow if self.oConfig.fDebug else None;
+ cHoursBack = self.oConfig.cHoursBack if self.oConfig.fDebug else 2;
+ oTestBoxLogic = TestBoxLogic(self.oDb);
+
+ #
+ # Generate a list of failures reasons we consider bad-testbox behavior.
+ #
+ aidFailureReasons = [
+ self.getFailureReason(self.ktReason_Host_DriverNotLoaded).idFailureReason,
+ self.getFailureReason(self.ktReason_Host_DriverNotUnloading).idFailureReason,
+ self.getFailureReason(self.ktReason_Host_DriverNotCompilable).idFailureReason,
+ self.getFailureReason(self.ktReason_Host_InstallationFailed).idFailureReason,
+ ];
+
+ #
+ # Get list of bad test boxes for given period and check them out individually.
+ #
+ aidBadTestBoxes = self.oTestSetLogic.fetchBadTestBoxIds(cHoursBack = cHoursBack, tsNow = tsNow,
+ aidFailureReasons = aidFailureReasons);
+ for idTestBox in aidBadTestBoxes:
+ # Skip if the testbox is already disabled or has a pending reboot command.
+ try:
+ oTestBox = TestBoxData().initFromDbWithId(self.oDb, idTestBox);
+ except Exception as oXcpt:
+ rcExit = self.eprint('Failed to get data for test box #%u in badTestBoxManagement: %s' % (idTestBox, oXcpt,));
+ continue;
+ if not oTestBox.fEnabled:
+ self.dprint(u'badTestBoxManagement: Skipping test box #%u (%s) as it has been disabled already.'
+ % ( idTestBox, oTestBox.sName, ));
+ continue;
+ if oTestBox.enmPendingCmd != TestBoxData.ksTestBoxCmd_None:
+ self.dprint(u'badTestBoxManagement: Skipping test box #%u (%s) as it has a command pending: %s'
+ % ( idTestBox, oTestBox.sName, oTestBox.enmPendingCmd));
+ continue;
+
+ # Get the most recent testsets for this box (descending on tsDone) and see how bad it is.
+ aoSets = self.oTestSetLogic.fetchSetsForTestBox(idTestBox, cHoursBack = cHoursBack, tsNow = tsNow);
+ cOkay = 0;
+ cBad = 0;
+ iFirstOkay = len(aoSets);
+ for iSet, oSet in enumerate(aoSets):
+ if oSet.enmStatus == TestSetData.ksTestStatus_BadTestBox:
+ cBad += 1;
+ else:
+ # Check for bad failure reasons.
+ oFailure = None;
+ if oSet.enmStatus in TestSetData.kasBadTestStatuses:
+ (oTree, _ ) = self.oTestResultLogic.fetchResultTree(oSet.idTestSet)
+ aoFailedResults = oTree.getListOfFailures();
+ for oFailedResult in aoFailedResults:
+ oFailure = self.oTestResultFailureLogic.getById(oFailedResult.idTestResult);
+ if oFailure is not None and oFailure.idFailureReason in aidFailureReasons:
+ break;
+ oFailure = None;
+ if oFailure is not None:
+ cBad += 1;
+ else:
+ # This is an okay test result then.
+ ## @todo maybe check the elapsed time here, it could still be a bad run?
+ cOkay += 1;
+ iFirstOkay = min(iFirstOkay, iSet);
+ if iSet > 10:
+ break;
+
+ # We react if there are two or more bad-testbox statuses at the head of the
+ # history and at least three in the last 10 results.
+ if iFirstOkay >= 2 and cBad > 2:
+ if oTestBoxLogic.hasTestBoxRecentlyBeenRebooted(idTestBox, cHoursBack = cHoursBack, tsNow = tsNow):
+ sComment = u'Disabling testbox #%u (%s) - iFirstOkay=%u cBad=%u cOkay=%u' \
+ % (idTestBox, oTestBox.sName, iFirstOkay, cBad, cOkay);
+ self.vprint(sComment);
+ self.sendEmailAlert(self.uidSelf, sComment);
+ if self.oConfig.fRealRun is True:
+ try:
+ oTestBoxLogic.disableTestBox(idTestBox, self.uidSelf, fCommit = True,
+ sComment = 'Automatically disabled (iFirstOkay=%u cBad=%u cOkay=%u)'
+ % (iFirstOkay, cBad, cOkay),);
+ except Exception as oXcpt:
+ rcExit = self.eprint(u'Error disabling testbox #%u (%u): %s\n' % (idTestBox, oTestBox.sName, oXcpt,));
+ else:
+ sComment = u'Rebooting testbox #%u (%s) - iFirstOkay=%u cBad=%u cOkay=%u' \
+ % (idTestBox, oTestBox.sName, iFirstOkay, cBad, cOkay);
+ self.vprint(sComment);
+ self.sendEmailAlert(self.uidSelf, sComment);
+ if self.oConfig.fRealRun is True:
+ try:
+ oTestBoxLogic.rebootTestBox(idTestBox, self.uidSelf, fCommit = True,
+ sComment = 'Automatically rebooted (iFirstOkay=%u cBad=%u cOkay=%u)'
+ % (iFirstOkay, cBad, cOkay),);
+ except Exception as oXcpt:
+ rcExit = self.eprint(u'Error rebooting testbox #%u (%s): %s\n' % (idTestBox, oTestBox.sName, oXcpt,));
+ else:
+ self.dprint(u'badTestBoxManagement: #%u (%s) looks ok: iFirstOkay=%u cBad=%u cOkay=%u'
+ % ( idTestBox, oTestBox.sName, iFirstOkay, cBad, cOkay));
+
+ ## @todo r=bird: review + rewrite;
+ ## - no selecting here, that belongs in the core/*.py files.
+ ## - preserve existing comments.
+ ## - doing way too much in the try/except block.
+ ## - No password quoting in the sshpass command that always fails (127).
+ ## - Timeout is way to low. testboxmem1 need more than 10 min to take a dump, ages to
+ ## get thru POST and another 5 just to time out in grub. Should be an hour or so.
+ ## Besides, it need to be constant elsewhere in the file, not a variable here.
+ ##
+ ##
+ ## Reset hanged testboxes
+ ##
+ #cStatusTimeoutMins = 10;
+ #
+ #self.oDb.execute('SELECT TestBoxStatuses.idTestBox\n'
+ # ' FROM TestBoxStatuses, TestBoxes\n'
+ # ' WHERE TestBoxStatuses.tsUpdated >= (CURRENT_TIMESTAMP - interval \'%s hours\')\n'
+ # ' AND TestBoxStatuses.tsUpdated < (CURRENT_TIMESTAMP - interval \'%s minutes\')\n'
+ # ' AND TestBoxStatuses.idTestBox = TestBoxes.idTestBox\n'
+ # ' AND Testboxes.tsExpire = \'infinity\'::timestamp', (cHoursBack,cStatusTimeoutMins));
+ #for idTestBox in self.oDb.fetchAll():
+ # idTestBox = idTestBox[0];
+ # try:
+ # oTestBox = TestBoxData().initFromDbWithId(self.oDb, idTestBox);
+ # except Exception as oXcpt:
+ # rcExit = self.eprint('Failed to get data for test box #%u in badTestBoxManagement: %s' % (idTestBox, oXcpt,));
+ # continue;
+ # # Skip if the testbox is already disabled, already reset or there's no iLOM
+ # if not oTestBox.fEnabled or oTestBox.ipLom is None or oTestBox.sComment is not None and oTestBox.sComment.find('Automatically reset') >= 0:
+ # self.dprint(u'badTestBoxManagement: Skipping test box #%u (%s) as it has been disabled already.'
+ # % ( idTestBox, oTestBox.sName, ));
+ # continue;
+ # ## @todo get iLOM credentials from a table?
+ # sCmd = 'sshpass -p%s ssh -oStrictHostKeyChecking=no root@%s show /SP && reset /SYS' % (g_ksLomPassword, oTestBox.ipLom,);
+ # try:
+ # oPs = subprocess.Popen(sCmd, stdout=subprocess.PIPE, shell=True);
+ # sStdout = oPs.communicate()[0];
+ # iRC = oPs.wait();
+ #
+ # oTestBox.sComment = 'Automatically reset (iRC=%u sStdout=%s)' % (iRC, sStdout,);
+ # oTestBoxLogic.editEntry(oTestBox, self.uidSelf, fCommit = True);
+ #
+ # sComment = u'Reset testbox #%u (%s) - iRC=%u sStduot=%s' % ( idTestBox, oTestBox.sName, iRC, sStdout);
+ # self.vprint(sComment);
+ # self.sendEmailAlert(self.uidSelf, sComment);
+ #
+ # except Exception as oXcpt:
+ # rcExit = self.eprint(u'Error resetting testbox #%u (%s): %s\n' % (idTestBox, oTestBox.sName, oXcpt,));
+ #
+ return rcExit;
+
+
+ ## @name Failure reasons we know.
+ ## @{
+
+ ktReason_Add_Installer_Win_Failed = ( 'Additions', 'Win GA install' );
+ ktReason_Add_ShFl_Automount = ( 'Additions', 'Automounting' );
+ ktReason_Add_ShFl_FsPerf = ( 'Additions', 'FsPerf' );
+ ktReason_Add_ShFl_FsPerf_Abend = ( 'Additions', 'FsPerf abend' );
+ ktReason_Add_GstCtl_Preparations = ( 'Additions', 'GstCtl preparations' );
+ ktReason_Add_GstCtl_SessionBasics = ( 'Additions', 'Session basics' );
+ ktReason_Add_GstCtl_SessionProcRefs = ( 'Additions', 'Session process' );
+ ktReason_Add_GstCtl_Session_Reboot = ( 'Additions', 'Session reboot' );
+ ktReason_Add_GstCtl_CopyFromGuest_Timeout = ( 'Additions', 'CopyFromGuest timeout' );
+ ktReason_Add_GstCtl_CopyToGuest_Timeout = ( 'Additions', 'CopyToGuest timeout' );
+ ktReason_Add_GstCtl_CopyToGuest_DstEmpty = ( 'Additions', 'CopyToGuest dst empty' );
+ ktReason_Add_GstCtl_CopyToGuest_DstExists = ( 'Additions', 'CopyToGuest dst exists' );
+ ktReason_Add_FlushViewOfFile = ( 'Additions', 'FlushViewOfFile' );
+ ktReason_Add_Mmap_Coherency = ( 'Additions', 'mmap coherency' );
+ ktReason_BSOD_Recovery = ( 'BSOD', 'Recovery' );
+ ktReason_BSOD_Automatic_Repair = ( 'BSOD', 'Automatic Repair' );
+ ktReason_BSOD_0000007F = ( 'BSOD', '0x0000007F' );
+ ktReason_BSOD_000000D1 = ( 'BSOD', '0x000000D1' );
+ ktReason_BSOD_C0000225 = ( 'BSOD', '0xC0000225 (boot)' );
+ ktReason_Guru_Generic = ( 'Guru Meditations', 'Generic Guru Meditation' );
+ ktReason_Guru_VERR_IEM_INSTR_NOT_IMPLEMENTED = ( 'Guru Meditations', 'VERR_IEM_INSTR_NOT_IMPLEMENTED' );
+ ktReason_Guru_VERR_IEM_ASPECT_NOT_IMPLEMENTED = ( 'Guru Meditations', 'VERR_IEM_ASPECT_NOT_IMPLEMENTED' );
+ ktReason_Guru_VERR_TRPM_DONT_PANIC = ( 'Guru Meditations', 'VERR_TRPM_DONT_PANIC' );
+ ktReason_Guru_VERR_PGM_PHYS_PAGE_RESERVED = ( 'Guru Meditations', 'VERR_PGM_PHYS_PAGE_RESERVED' );
+ ktReason_Guru_VERR_VMX_INVALID_GUEST_STATE = ( 'Guru Meditations', 'VERR_VMX_INVALID_GUEST_STATE' );
+ ktReason_Guru_VINF_EM_TRIPLE_FAULT = ( 'Guru Meditations', 'VINF_EM_TRIPLE_FAULT' );
+ ktReason_Host_HostMemoryLow = ( 'Host', 'HostMemoryLow' );
+ ktReason_Host_DriverNotLoaded = ( 'Host', 'Driver not loaded' );
+ ktReason_Host_DriverNotUnloading = ( 'Host', 'Driver not unloading' );
+ ktReason_Host_DriverNotCompilable = ( 'Host', 'Driver not compilable' );
+ ktReason_Host_InstallationFailed = ( 'Host', 'Installation failed' );
+ ktReason_Host_InstallationWantReboot = ( 'Host', 'Installation want reboot' );
+ ktReason_Host_InvalidPackage = ( 'Host', 'ERROR_INSTALL_PACKAGE_INVALID' );
+ ktReason_Host_InstallSourceAbsent = ( 'Host', 'ERROR_INSTALL_SOURCE_ABSENT' );
+ ktReason_Host_NotSignedWithBuildCert = ( 'Host', 'Not signed with build cert' );
+ ktReason_Host_DiskFull = ( 'Host', 'Host disk full' );
+ ktReason_Host_DoubleFreeHeap = ( 'Host', 'Double free or corruption' );
+ ktReason_Host_LeftoverService = ( 'Host', 'Leftover service' );
+ ktReason_Host_win32com_gen_py = ( 'Host', 'win32com.gen_py' );
+ ktReason_Host_Reboot_OSX_Watchdog_Timeout = ( 'Host Reboot', 'OSX Watchdog Timeout' );
+ ktReason_Host_Modprobe_Failed = ( 'Host', 'Modprobe failed' );
+ ktReason_Host_Install_Hang = ( 'Host', 'Install hang' );
+ ktReason_Host_NetworkMisconfiguration = ( 'Host', 'Network misconfiguration' );
+ ktReason_Host_TSTInfo_Accuracy_OOR = ( 'Host', 'TSTInfo accuracy out of range' );
+ ktReason_Networking_Nonexistent_host_nic = ( 'Networking', 'Nonexistent host networking interface' );
+ ktReason_Networking_VERR_INTNET_FLT_IF_NOT_FOUND = ( 'Networking', 'VERR_INTNET_FLT_IF_NOT_FOUND' );
+ ktReason_OSInstall_GRUB_hang = ( 'O/S Install', 'GRUB hang' );
+ ktReason_OSInstall_Udev_hang = ( 'O/S Install', 'udev hang' );
+ ktReason_OSInstall_Sata_no_BM = ( 'O/S Install', 'SATA busmaster bit not set' );
+ ktReason_Panic_BootManagerC000000F = ( 'Panic', 'Hardware Changed' );
+ ktReason_Panic_MP_BIOS_IO_APIC = ( 'Panic', 'MP-BIOS/IO-APIC' );
+ ktReason_Panic_HugeMemory = ( 'Panic', 'Huge memory assertion' );
+ ktReason_Panic_IOAPICDoesntWork = ( 'Panic', 'IO-APIC and timer does not work' );
+ ktReason_Panic_TxUnitHang = ( 'Panic', 'Tx Unit Hang' );
+ ktReason_API_std_bad_alloc = ( 'API / (XP)COM', 'std::bad_alloc' );
+ ktReason_API_Digest_Mismatch = ( 'API / (XP)COM', 'Digest mismatch' );
+ ktReason_API_MoveVM_SharingViolation = ( 'API / (XP)COM', 'MoveVM sharing violation' );
+ ktReason_API_MoveVM_InvalidParameter = ( 'API / (XP)COM', 'MoveVM invalid parameter' );
+ ktReason_API_Open_Session_Failed = ( 'API / (XP)COM', 'Open session failed' );
+ ktReason_XPCOM_Exit_Minus_11 = ( 'API / (XP)COM', 'exit -11' );
+ ktReason_XPCOM_VBoxSVC_Hang = ( 'API / (XP)COM', 'VBoxSVC hang' );
+ ktReason_XPCOM_VBoxSVC_Hang_Plus_Heap_Corruption = ( 'API / (XP)COM', 'VBoxSVC hang + heap corruption' );
+ ktReason_XPCOM_NS_ERROR_CALL_FAILED = ( 'API / (XP)COM', 'NS_ERROR_CALL_FAILED' );
+ ktReason_BootManager_Image_corrupt = ( 'Unknown', 'BOOTMGR Image corrupt' );
+ ktReason_Unknown_Heap_Corruption = ( 'Unknown', 'Heap corruption' );
+ ktReason_Unknown_Reboot_Loop = ( 'Unknown', 'Reboot loop' );
+ ktReason_Unknown_File_Not_Found = ( 'Unknown', 'File not found' );
+ ktReason_Unknown_HalReturnToFirmware = ( 'Unknown', 'HalReturnToFirmware' );
+ ktReason_Unknown_VM_Crash = ( 'Unknown', 'VM crash' );
+ ktReason_Unknown_VM_Terminated = ( 'Unknown', 'VM terminated' );
+ ktReason_Unknown_VM_Start_Error = ( 'Unknown', 'VM Start Error' );
+ ktReason_Unknown_VM_Runtime_Error = ( 'Unknown', 'VM Runtime Error' );
+ ktReason_VMM_kvm_lock_spinning = ( 'VMM', 'kvm_lock_spinning' );
+ ktReason_Ignore_Buggy_Test_Driver = ( 'Ignore', 'Buggy test driver' );
+ ktReason_Ignore_Stale_Files = ( 'Ignore', 'Stale files' );
+ ktReason_Buggy_Build_Broken_Build = ( 'Broken Build', 'Buggy build' );
+ ktReason_GuestBug_CompizVBoxQt = ( 'Guest Bug', 'Compiz + VirtualBox Qt GUI crash' );
+ ## @}
+
+ ## BSOD category.
+ ksBsodCategory = 'BSOD';
+ ## Special reason indicating that the flesh and blood sheriff has work to do.
+ ksBsodAddNew = 'Add new BSOD';
+
+ ## Unit test category.
+ ksUnitTestCategory = 'Unit';
+ ## Special reason indicating that the flesh and blood sheriff has work to do.
+ ksUnitTestAddNew = 'Add new';
+
+ ## Used for indica that we shouldn't report anything for this test result ID and
+ ## consider promoting the previous error to test set level if it's the only one.
+ ktHarmless = ( 'Probably', 'Caused by previous error' );
+
+
+ def caseClosed(self, oCaseFile):
+ """
+ Reports the findings in the case and closes it.
+ """
+ #
+ # Log it and create a dReasonForReasultId we can use below.
+ #
+ dCommentForResultId = oCaseFile.dCommentForResultId;
+ if oCaseFile.dReasonForResultId:
+ # Must weed out ktHarmless.
+ dReasonForResultId = {};
+ for idKey, tReason in oCaseFile.dReasonForResultId.items():
+ if tReason is not self.ktHarmless:
+ dReasonForResultId[idKey] = tReason;
+ if not dReasonForResultId:
+ self.vprint(u'TODO: Closing %s without a real reason, only %s.'
+ % (oCaseFile.sName, oCaseFile.dReasonForResultId));
+ return False;
+
+ # Try promote to single reason.
+ atValues = dReasonForResultId.values();
+ fSingleReason = True;
+ if len(dReasonForResultId) == 1 and next(iter(dReasonForResultId.keys())) != oCaseFile.oTestSet.idTestResult:
+ self.dprint(u'Promoting single reason to whole set: %s' % (next(iter(atValues)),));
+ elif len(dReasonForResultId) > 1 and len(atValues) == list(atValues).count(next(iter(atValues))):
+ self.dprint(u'Merged %d reasons to a single one: %s' % (len(atValues), next(iter(atValues))));
+ else:
+ fSingleReason = False;
+ if fSingleReason:
+ dReasonForResultId = { oCaseFile.oTestSet.idTestResult: next(iter(atValues)), };
+ if dCommentForResultId:
+ dCommentForResultId = { oCaseFile.oTestSet.idTestResult: next(iter(dCommentForResultId.values())), };
+ elif oCaseFile.tReason is not None:
+ dReasonForResultId = { oCaseFile.oTestSet.idTestResult: oCaseFile.tReason, };
+ else:
+ self.vprint(u'Closing %s without a reason - this should not happen!' % (oCaseFile.sName,));
+ return False;
+
+ self.vprint(u'Closing %s with following reason%s: %s'
+ % ( oCaseFile.sName, 's' if len(dReasonForResultId) > 1 else '', dReasonForResultId, ));
+
+ #
+ # Add the test failure reason record(s).
+ #
+ for idTestResult, tReason in dReasonForResultId.items():
+ oFailureReason = self.getFailureReason(tReason);
+ if oFailureReason is not None:
+ sComment = 'Set by $Revision: 155244 $' # Handy for reverting later.
+ if idTestResult in dCommentForResultId:
+ sComment += ': ' + dCommentForResultId[idTestResult];
+
+ oAdd = TestResultFailureData();
+ oAdd.initFromValues(idTestResult = idTestResult,
+ idFailureReason = oFailureReason.idFailureReason,
+ uidAuthor = self.uidSelf,
+ idTestSet = oCaseFile.oTestSet.idTestSet,
+ sComment = sComment,);
+ if self.oConfig.fRealRun:
+ try:
+ self.oTestResultFailureLogic.addEntry(oAdd, self.uidSelf, fCommit = True);
+ except Exception as oXcpt:
+ self.eprint(u'caseClosed: Exception "%s" while adding reason %s for %s'
+ % (oXcpt, oAdd, oCaseFile.sLongName,));
+ else:
+ self.eprint(u'caseClosed: Cannot locate failure reason: %s / %s' % ( tReason[0], tReason[1],));
+ return True;
+
+ #
+ # Tools for assiting log parsing.
+ #
+
+ @staticmethod
+ def matchFollowedByLines(sStr, off, asFollowingLines):
+ """ Worker for isThisFollowedByTheseLines. """
+
+ # Advance off to the end of the line.
+ off = sStr.find('\n', off);
+ if off < 0:
+ return False;
+ off += 1;
+
+ # Match each string with the subsequent lines.
+ for iLine, sLine in enumerate(asFollowingLines):
+ offEnd = sStr.find('\n', off);
+ if offEnd < 0:
+ return iLine + 1 == len(asFollowingLines) and sStr.find(sLine, off) < 0;
+ if sLine and sStr.find(sLine, off, offEnd) < 0:
+ return False;
+
+ # next line.
+ off = offEnd + 1;
+
+ return True;
+
+ @staticmethod
+ def isThisFollowedByTheseLines(sStr, sFirst, asFollowingLines):
+ """
+ Looks for a line contining sFirst which is then followed by lines
+ with the strings in asFollowingLines. (No newline chars anywhere!)
+ Returns True / False.
+ """
+ off = sStr.find(sFirst, 0);
+ while off >= 0:
+ if VirtualTestSheriff.matchFollowedByLines(sStr, off, asFollowingLines):
+ return True;
+ off = sStr.find(sFirst, off + 1);
+ return False;
+
+ @staticmethod
+ def findAndReturnRestOfLine(sHaystack, sNeedle):
+ """
+ Looks for sNeedle in sHaystack.
+ Returns The text following the needle up to the end of the line.
+ Returns None if not found.
+ """
+ if sHaystack is None:
+ return None;
+ off = sHaystack.find(sNeedle);
+ if off < 0:
+ return None;
+ off += len(sNeedle)
+ offEol = sHaystack.find('\n', off);
+ if offEol < 0:
+ offEol = len(sHaystack);
+ return sHaystack[off:offEol]
+
+ @staticmethod
+ def findInAnyAndReturnRestOfLine(asHaystacks, sNeedle):
+ """
+ Looks for sNeedle in zeroe or more haystacks (asHaystack).
+ Returns The text following the first needed found up to the end of the line.
+ Returns None if not found.
+ """
+ for sHaystack in asHaystacks:
+ sRet = VirtualTestSheriff.findAndReturnRestOfLine(sHaystack, sNeedle);
+ if sRet is not None:
+ return sRet;
+ return None;
+
+
+ #
+ # The investigative units.
+ #
+
+ katSimpleInstallUninstallMainLogReasons = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( False, ktReason_Host_LeftoverService,
+ 'SERVICE_NAME: vbox' ),
+ ( False, ktReason_Host_LeftoverService,
+ 'Seems installation was skipped. Old version lurking behind? Not the fault of this build/test run!'),
+ ];
+
+ kdatSimpleInstallUninstallMainLogReasonsPerOs = {
+ 'darwin': [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( True, ktReason_Host_DriverNotUnloading,
+ 'Can\'t remove kext org.virtualbox.kext.VBoxDrv; services failed to terminate - 0xe00002c7' ),
+ ],
+ 'linux': [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( True, ktReason_Host_DriverNotCompilable,
+ 'This system is not currently set up to build kernel modules' ),
+ ( True, ktReason_Host_DriverNotCompilable,
+ 'This system is currently not set up to build kernel modules' ),
+ ( True, ktReason_Host_InstallationFailed,
+ 'vboxdrv.sh: failed: Look at /var/log/vbox-install.log to find out what went wrong.' ),
+ ( True, ktReason_Host_DriverNotUnloading,
+ 'Cannot unload module vboxdrv'),
+ ],
+ 'solaris': [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( True, ktReason_Host_DriverNotUnloading, 'can\'t unload the module: Device busy' ),
+ ( True, ktReason_Host_DriverNotUnloading, 'Unloading: Host module ...FAILED!' ),
+ ( True, ktReason_Host_DriverNotUnloading, 'Unloading: NetFilter (Crossbow) module ...FAILED!' ),
+ ( True, ktReason_Host_InstallationFailed, 'svcadm: Couldn\'t bind to svc.configd.' ),
+ ( True, ktReason_Host_InstallationFailed, 'pkgadd: ERROR: postinstall script did not complete successfully' ),
+ ],
+ 'win': [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( True, ktReason_Host_InstallationWantReboot, 'ERROR_SUCCESS_REBOOT_REQUIRED' ),
+ ( False, ktReason_Host_InstallationFailed, 'Installation error.' ),
+ ( True, ktReason_Host_InvalidPackage, 'Uninstaller failed, exit code: 1620' ),
+ ( True, ktReason_Host_InstallSourceAbsent, 'Uninstaller failed, exit code: 1612' ),
+ ],
+ };
+
+
+ def investigateInstallUninstallFailure(self, oCaseFile, oFailedResult, sResultLog, fInstall):
+ """
+ Investigates an install or uninstall failure.
+
+ We lump the two together since the installation typically also performs
+ an uninstall first and will be seeing similar issues to the uninstall.
+ """
+ self.dprint(u'%s + %s <<\n%s\n<<' % (oFailedResult.tsCreated, oFailedResult.tsElapsed, sResultLog,));
+
+ if fInstall and oFailedResult.enmStatus == TestSetData.ksTestStatus_TimedOut:
+ oCaseFile.noteReasonForId(self.ktReason_Host_Install_Hang, oFailedResult.idTestResult)
+ return True;
+
+ atSimple = self.katSimpleInstallUninstallMainLogReasons;
+ if oCaseFile.oTestBox.sOs in self.kdatSimpleInstallUninstallMainLogReasonsPerOs:
+ atSimple = self.kdatSimpleInstallUninstallMainLogReasonsPerOs[oCaseFile.oTestBox.sOs] + atSimple;
+
+ fFoundSomething = False;
+ for fStopOnHit, tReason, sNeedle in atSimple:
+ if sResultLog.find(sNeedle) > 0:
+ oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult);
+ if fStopOnHit:
+ return True;
+ fFoundSomething = True;
+
+ return fFoundSomething if fFoundSomething else None;
+
+
+ def investigateBadTestBox(self, oCaseFile):
+ """
+ Checks out bad-testbox statuses.
+ """
+ _ = oCaseFile;
+ return False;
+
+
+ def investigateVBoxUnitTest(self, oCaseFile):
+ """
+ Checks out a VBox unittest problem.
+ """
+
+ #
+ # Process simple test case failures first, using their name as reason.
+ # We do the reason management just like for BSODs.
+ #
+ cRelevantOnes = 0;
+ sMainLog = oCaseFile.getMainLog();
+ aoFailedResults = oCaseFile.oTree.getListOfFailures();
+ for oFailedResult in aoFailedResults:
+ if oFailedResult is oCaseFile.oTree:
+ self.vprint('TODO: toplevel failure');
+ cRelevantOnes += 1
+
+ elif oFailedResult.sName == 'Installing VirtualBox':
+ sResultLog = TestSetData.extractLogSectionElapsed(sMainLog, oFailedResult.tsCreated, oFailedResult.tsElapsed);
+ self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = True)
+ cRelevantOnes += 1
+
+ elif oFailedResult.sName == 'Uninstalling VirtualBox':
+ sResultLog = TestSetData.extractLogSectionElapsed(sMainLog, oFailedResult.tsCreated, oFailedResult.tsElapsed);
+ self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = False)
+ cRelevantOnes += 1
+
+ elif oFailedResult.oParent is not None:
+ # Get the 2nd level node because that's where we'll find the unit test name.
+ while oFailedResult.oParent.oParent is not None:
+ oFailedResult = oFailedResult.oParent;
+
+ # Only report a failure once.
+ if oFailedResult.idTestResult not in oCaseFile.dReasonForResultId:
+ sKey = oFailedResult.sName;
+ if sKey.startswith('testcase/'):
+ sKey = sKey[9:];
+ if sKey in self.asUnitTestReasons:
+ tReason = ( self.ksUnitTestCategory, sKey );
+ oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult);
+ else:
+ self.dprint(u'Unit test failure "%s" not found in %s;' % (sKey, self.asUnitTestReasons));
+ tReason = ( self.ksUnitTestCategory, self.ksUnitTestAddNew );
+ oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult, sComment = sKey);
+ cRelevantOnes += 1
+ else:
+ self.vprint(u'Internal error: expected oParent to NOT be None for %s' % (oFailedResult,));
+
+ #
+ # If we've caught all the relevant ones by now, report the result.
+ #
+ if len(oCaseFile.dReasonForResultId) >= cRelevantOnes:
+ return self.caseClosed(oCaseFile);
+ return False;
+
+ def extractGuestCpuStack(self, sInfoText):
+ """
+ Extracts the guest CPU stacks from the input file.
+
+ Returns a dictionary keyed by the CPU number, value being a list of
+ raw stack lines (no header).
+ Returns empty dictionary if no stacks where found.
+ """
+ dRet = {};
+ off = 0;
+ while True:
+ # Find the stack.
+ offStart = sInfoText.find('=== start guest stack VCPU ', off);
+ if offStart < 0:
+ break;
+ offEnd = sInfoText.find('=== end guest stack', offStart + 20);
+ if offEnd >= 0:
+ offEnd += 3;
+ else:
+ offEnd = sInfoText.find('=== start guest stack VCPU', offStart + 20);
+ if offEnd < 0:
+ offEnd = len(sInfoText);
+
+ sStack = sInfoText[offStart : offEnd];
+ sStack = sStack.replace('\r',''); # paranoia
+ asLines = sStack.split('\n');
+
+ # Figure the CPU.
+ asWords = asLines[0].split();
+ if len(asWords) < 6 or not asWords[5].isdigit():
+ break;
+ iCpu = int(asWords[5]);
+
+ # Add it and advance.
+ dRet[iCpu] = [sLine.rstrip() for sLine in asLines[2:-1]]
+ off = offEnd;
+ return dRet;
+
+ def investigateInfoKvmLockSpinning(self, oCaseFile, sInfoText, dLogs):
+ """ Investigates kvm_lock_spinning deadlocks """
+ #
+ # Extract the stacks. We need more than one CPU to create a deadlock.
+ #
+ dStacks = self.extractGuestCpuStack(sInfoText);
+ self.dprint('kvm_lock_spinning: found %s stacks' % (len(dStacks),));
+ if len(dStacks) >= 2:
+ #
+ # Examin each of the stacks. Each must have kvm_lock_spinning in
+ # one of the first three entries.
+ #
+ cHits = 0;
+ for asBacktrace in dStacks.values():
+ for iFrame in xrange(min(3, len(asBacktrace))):
+ if asBacktrace[iFrame].find('kvm_lock_spinning') >= 0:
+ cHits += 1;
+ break;
+ self.dprint('kvm_lock_spinning: %s/%s hits' % (cHits, len(dStacks),));
+ if cHits == len(dStacks):
+ return (True, self.ktReason_VMM_kvm_lock_spinning);
+
+ _ = dLogs; _ = oCaseFile;
+ return (False, None);
+
+ def investigateInfoHalReturnToFirmware(self, oCaseFile, sInfoText, dLogs):
+ """ Investigates HalReturnToFirmware hangs """
+ del oCaseFile
+ del sInfoText
+ del dLogs
+ # hope that's sufficient
+ return (True, self.ktReason_Unknown_HalReturnToFirmware);
+
+ ## Things we search a main or VM log for to figure out why something went bust.
+ ## @note DO NOT ADD MORE STUFF HERE!
+ ## Please use katSimpleMainLogReasons and katSimpleVmLogReasons instead!
+ katSimpleMainAndVmLogReasonsDeprecated = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( False, ktReason_Guru_Generic, 'GuruMeditation' ),
+ ( False, ktReason_Guru_Generic, 'Guru Meditation' ),
+ ( True, ktReason_Guru_VERR_IEM_INSTR_NOT_IMPLEMENTED, 'VERR_IEM_INSTR_NOT_IMPLEMENTED' ),
+ ( True, ktReason_Guru_VERR_IEM_ASPECT_NOT_IMPLEMENTED, 'VERR_IEM_ASPECT_NOT_IMPLEMENTED' ),
+ ( True, ktReason_Guru_VERR_TRPM_DONT_PANIC, 'VERR_TRPM_DONT_PANIC' ),
+ ( True, ktReason_Guru_VERR_PGM_PHYS_PAGE_RESERVED, 'VERR_PGM_PHYS_PAGE_RESERVED' ),
+ ( True, ktReason_Guru_VERR_VMX_INVALID_GUEST_STATE, 'VERR_VMX_INVALID_GUEST_STATE' ),
+ ( True, ktReason_Guru_VINF_EM_TRIPLE_FAULT, 'VINF_EM_TRIPLE_FAULT' ),
+ ( True, ktReason_Networking_Nonexistent_host_nic,
+ 'rc=E_FAIL text="Nonexistent host networking interface, name \'eth0\' (VERR_INTERNAL_ERROR)"' ),
+ ( True, ktReason_Networking_VERR_INTNET_FLT_IF_NOT_FOUND,
+ 'Failed to attach the network LUN (VERR_INTNET_FLT_IF_NOT_FOUND)' ),
+ ( True, ktReason_Host_Reboot_OSX_Watchdog_Timeout, ': "OSX Watchdog Timeout: ' ),
+ ( False, ktReason_XPCOM_NS_ERROR_CALL_FAILED,
+ 'Exception: 0x800706be (Call to remote object failed (NS_ERROR_CALL_FAILED))' ),
+ ( True, ktReason_API_std_bad_alloc, 'Unexpected exception: std::bad_alloc' ),
+ ( True, ktReason_Host_HostMemoryLow, 'HostMemoryLow' ),
+ ( True, ktReason_Host_HostMemoryLow, 'Failed to procure handy pages; rc=VERR_NO_MEMORY' ),
+ ( True, ktReason_Unknown_File_Not_Found,
+ 'Error: failed to start machine. Error message: File not found. (VERR_FILE_NOT_FOUND)' ),
+ ( True, ktReason_Unknown_File_Not_Found, # lump it in with file-not-found for now.
+ 'Error: failed to start machine. Error message: Not supported. (VERR_NOT_SUPPORTED)' ),
+ ( False, ktReason_Unknown_VM_Crash, 'txsDoConnectViaTcp: Machine state: Aborted' ),
+ ( True, ktReason_Host_Modprobe_Failed, 'Kernel driver not installed' ),
+ ( True, ktReason_OSInstall_Sata_no_BM, 'PCHS=14128/14134/8224' ),
+ ( True, ktReason_Host_DoubleFreeHeap, 'double free or corruption' ),
+ #( False, ktReason_Unknown_VM_Start_Error, 'VMSetError: ' ), - false positives for stuff like:
+ # "VMSetError: VD: Backend 'VBoxIsoMaker' does not support async I/O"
+ ( False, ktReason_Unknown_VM_Start_Error, 'error: failed to open session for' ),
+ ( False, ktReason_Unknown_VM_Runtime_Error, 'Console: VM runtime error: fatal=true' ),
+ ];
+
+ ## This we search a main log for to figure out why something went bust.
+ katSimpleMainLogReasons = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( False, ktReason_Host_win32com_gen_py, 'ModuleNotFoundError: No module named \'win32com.gen_py' ),
+
+ ];
+
+ ## This we search a VM log for to figure out why something went bust.
+ katSimpleVmLogReasons = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ # Note: Works for ATA and VD drivers.
+ ( False, ktReason_Host_DiskFull, '_DISKFULL' ),
+ ];
+
+ ## Things we search a VBoxHardening.log file for to figure out why something went bust.
+ katSimpleVBoxHardeningLogReasons = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( True, ktReason_Host_DriverNotLoaded, 'Error opening VBoxDrvStub: STATUS_OBJECT_NAME_NOT_FOUND' ),
+ ( True, ktReason_Host_NotSignedWithBuildCert, 'Not signed with the build certificate' ),
+ ( True, ktReason_Host_TSTInfo_Accuracy_OOR, 'RTCRTSPTSTINFO::Accuracy::Millis: Out of range' ),
+ ( False, ktReason_Unknown_VM_Crash, 'Quitting: ExitCode=0xc0000005 (rcNtWait=' ),
+ ];
+
+ ## Things we search a kernel.log file for to figure out why something went bust.
+ katSimpleKernelLogReasons = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( True, ktReason_Panic_HugeMemory, 'mm/huge_memory.c:1988' ),
+ ( True, ktReason_Panic_IOAPICDoesntWork, 'IO-APIC + timer doesn\'t work' ),
+ ( True, ktReason_Panic_TxUnitHang, 'Detected Tx Unit Hang' ),
+ ( True, ktReason_GuestBug_CompizVBoxQt, 'error 4 in libQt5CoreVBox' ),
+ ( True, ktReason_GuestBug_CompizVBoxQt, 'error 4 in libgtk-3' ),
+ ];
+
+ ## Things we search the _RIGHT_ _STRIPPED_ vgatext for.
+ katSimpleVgaTextReasons = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( True, ktReason_Panic_MP_BIOS_IO_APIC,
+ "..MP-BIOS bug: 8254 timer not connected to IO-APIC\n\n" ),
+ ( True, ktReason_Panic_MP_BIOS_IO_APIC,
+ "..MP-BIOS bug: 8254 timer not connected to IO-APIC\n"
+ "...trying to set up timer (IRQ0) through the 8259A ... failed.\n"
+ "...trying to set up timer as Virtual Wire IRQ... failed.\n"
+ "...trying to set up timer as ExtINT IRQ... failed :(.\n"
+ "Kernel panic - not syncing: IO-APIC + timer doesn't work! Boot with apic=debug\n"
+ "and send a report. Then try booting with the 'noapic' option\n"
+ "\n" ),
+ ( True, ktReason_OSInstall_GRUB_hang,
+ "-----\nGRUB Loading stage2..\n\n\n\n" ),
+ ( True, ktReason_OSInstall_GRUB_hang,
+ "-----\nGRUB Loading stage2...\n\n\n\n" ), # the 3 dot hang appears to be less frequent
+ ( True, ktReason_OSInstall_GRUB_hang,
+ "-----\nGRUB Loading stage2....\n\n\n\n" ), # the 4 dot hang appears to be very infrequent
+ ( True, ktReason_OSInstall_GRUB_hang,
+ "-----\nGRUB Loading stage2.....\n\n\n\n" ), # the 5 dot hang appears to be more frequent again
+ ( True, ktReason_OSInstall_Udev_hang,
+ "\nStarting udev:\n\n\n\n" ),
+ ( True, ktReason_OSInstall_Udev_hang,
+ "\nStarting udev:\n------" ),
+ ( True, ktReason_Panic_BootManagerC000000F,
+ "Windows failed to start. A recent hardware or software change might be the" ),
+ ( True, ktReason_BootManager_Image_corrupt,
+ "BOOTMGR image is corrupt. The system cannot boot." ),
+ ];
+
+ ## Things we search for in the info.txt file. Require handlers for now.
+ katInfoTextHandlers = [
+ # ( Trigger text, handler method )
+ ( "kvm_lock_spinning", investigateInfoKvmLockSpinning ),
+ ( "HalReturnToFirmware", investigateInfoHalReturnToFirmware ),
+ ];
+
+ ## Mapping screenshot/failure SHA-256 hashes to failure reasons.
+ katSimpleScreenshotHashReasons = [
+ # ( Whether to stop on hit, reason tuple, lowercased sha-256 of PIL.Image.tostring output )
+ ( True, ktReason_BSOD_Recovery, '576f8e38d62b311cac7e3dc3436a0d0b9bd8cfd7fa9c43aafa95631520a45eac' ),
+ ( True, ktReason_BSOD_Automatic_Repair, 'c6a72076cc619937a7a39cfe9915b36d94cee0d4e3ce5ce061485792dcee2749' ),
+ ( True, ktReason_BSOD_Automatic_Repair, '26c4d8a724ff2c5e1051f3d5b650dbda7b5fdee0aa3e3c6059797f7484a515df' ),
+ ( True, ktReason_BSOD_0000007F, '57e1880619e13042a87100e7a38c8974b85ce3866501be621bea0cc696bb2c63' ),
+ ( True, ktReason_BSOD_000000D1, '134621281f00a3f8aeeb7660064bffbf6187ed56d5852142328d0bcb18ef0ede' ),
+ ( True, ktReason_BSOD_000000D1, '279f11258150c9d2fef041eca65501f3141da8df39256d8f6377e897e3b45a93' ),
+ ( True, ktReason_BSOD_C0000225, 'bd13a144be9dcdfb16bc863ff4c8f02a86e263c174f2cd5ffd27ca5f3aa31789' ),
+ ( True, ktReason_BSOD_C0000225, '8348b465e7ee9e59dd4e785880c57fd8677de05d11ac21e786bfde935307b42f' ),
+ ( True, ktReason_BSOD_C0000225, '1316e1fc818a73348412788e6910b8c016f237d8b4e15b20caf4a866f7a7840e' ),
+ ( True, ktReason_BSOD_C0000225, '54e0acbff365ce20a85abbe42bcd53647b8b9e80c68e45b2cd30e86bf177a0b5' ),
+ ( True, ktReason_BSOD_C0000225, '50fec50b5199923fa48b3f3e782687cc381e1c8a788ebda14e6a355fbe3bb1b3' ),
+ ];
+
+
+ def scanLog(self, asLogs, atNeedles, oCaseFile, idTestResult):
+ """
+ Scans for atNeedles in sLog.
+
+ Returns True if a stop-on-hit neelde was found.
+ Returns None if a no-stop reason was found.
+ Returns False if no hit.
+ """
+ fRet = False;
+ for fStopOnHit, tReason, oNeedle in atNeedles:
+ fMatch = False;
+ if utils.isString(oNeedle):
+ for sLog in asLogs:
+ if sLog:
+ fMatch |= sLog.find(oNeedle) > 0;
+ else:
+ for sLog in asLogs:
+ if sLog:
+ fMatch |= oNeedle.search(sLog) is not None;
+ if fMatch:
+ oCaseFile.noteReasonForId(tReason, idTestResult);
+ if fStopOnHit:
+ return True;
+ fRet = None;
+ return fRet;
+
+
+ def investigateGATest(self, oCaseFile, oFailedResult, sResultLog):
+ """
+ Investigates a failed VM run.
+ """
+ enmReason = None;
+ sParentName = oFailedResult.oParent.sName if oFailedResult.oParent else '';
+ if oFailedResult.sName == 'VBoxWindowsAdditions.exe' or sResultLog.find('VBoxWindowsAdditions.exe" failed with') > 0:
+ enmReason = self.ktReason_Add_Installer_Win_Failed;
+ # guest control:
+ elif sParentName == 'Guest Control' and oFailedResult.sName == 'Preparations':
+ enmReason = self.ktReason_Add_GstCtl_Preparations;
+ elif oFailedResult.sName == 'Session Basics':
+ enmReason = self.ktReason_Add_GstCtl_SessionBasics;
+ elif oFailedResult.sName == 'Session Process References':
+ enmReason = self.ktReason_Add_GstCtl_SessionProcRefs;
+ elif oFailedResult.sName == 'Copy from guest':
+ if sResultLog.find('*** abort action ***') >= 0:
+ enmReason = self.ktReason_Add_GstCtl_CopyFromGuest_Timeout;
+ elif oFailedResult.sName == 'Copy to guest':
+ off = sResultLog.find('"Guest directory "');
+ if off > 0 and sResultLog.find('" already exists"', off, off + 80):
+ enmReason = self.ktReason_Add_GstCtl_CopyToGuest_DstExists;
+ elif sResultLog.find('Guest destination must not be empty') >= 0:
+ enmReason = self.ktReason_Add_GstCtl_CopyToGuest_DstEmpty;
+ elif sResultLog.find('*** abort action ***') >= 0:
+ enmReason = self.ktReason_Add_GstCtl_CopyToGuest_Timeout;
+ elif oFailedResult.sName.find('Session w/ Guest Reboot') >= 0:
+ enmReason = self.ktReason_Add_GstCtl_Session_Reboot;
+ # shared folders:
+ elif sParentName == 'Shared Folders' and oFailedResult.sName == 'Automounting':
+ enmReason = self.ktReason_Add_ShFl_Automount;
+ elif oFailedResult.sName == 'mmap':
+ if sResultLog.find('FsPerf: Flush issue at offset ') >= 0:
+ enmReason = self.ktReason_Add_Mmap_Coherency;
+ elif sResultLog.find('FlushViewOfFile') >= 0:
+ enmReason = self.ktReason_Add_FlushViewOfFile;
+ elif sParentName == 'Shared Folders' and oFailedResult.sName == 'Running FsPerf':
+ enmReason = self.ktReason_Add_ShFl_FsPerf; ## Maybe it would be better to be more specific...
+
+ if enmReason is not None:
+ return oCaseFile.noteReasonForId(enmReason, oFailedResult.idTestResult);
+
+ self.vprint(u'TODO: Cannot place GA failure idTestResult=%u - %s' % (oFailedResult.idTestResult, oFailedResult.sName,));
+ self.dprint(u'%s + %s <<\n%s\n<<' % (oFailedResult.tsCreated, oFailedResult.tsElapsed, sResultLog,));
+ return False;
+
+ def isResultFromGATest(self, oCaseFile, oFailedResult):
+ """
+ Checks if this result and corresponding log snippet looks like a GA test run.
+ """
+ while oFailedResult is not None:
+ if oFailedResult.sName in [ 'Guest Control', 'Shared Folders', 'FsPerf', 'VBoxWindowsAdditions.exe' ]:
+ return True;
+ if oCaseFile.oTestCase.sName == 'Guest Additions' and oFailedResult.sName in [ 'Install', ]:
+ return True;
+ oFailedResult = oFailedResult.oParent;
+ return False;
+
+
+ def investigateVMResult(self, oCaseFile, oFailedResult, sResultLog):
+ """
+ Investigates a failed VM run.
+ """
+
+ def investigateLogSet():
+ """
+ Investigates the current set of VM related logs.
+ """
+ self.dprint('investigateLogSet: log lengths: result %u, VM %u, kernel %u, vga text %u, info text %u, hard %u'
+ % ( len(sResultLog if sResultLog else ''),
+ len(sVMLog if sVMLog else ''),
+ len(sKrnlLog if sKrnlLog else ''),
+ len(sVgaText if sVgaText else ''),
+ len(sInfoText if sInfoText else ''),
+ len(sNtHardLog if sNtHardLog else ''),));
+
+ #self.dprint(u'main.log<<<\n%s\n<<<\n' % (sResultLog,));
+ #self.dprint(u'vbox.log<<<\n%s\n<<<\n' % (sVMLog,));
+ #self.dprint(u'krnl.log<<<\n%s\n<<<\n' % (sKrnlLog,));
+ #self.dprint(u'vgatext.txt<<<\n%s\n<<<\n' % (sVgaText,));
+ #self.dprint(u'info.txt<<<\n%s\n<<<\n' % (sInfoText,));
+ #self.dprint(u'hard.txt<<<\n%s\n<<<\n' % (sNtHardLog,));
+
+ # TODO: more
+
+ #
+ # Look for BSODs. Some stupid stupid inconsistencies in reason and log messages here, so don't try prettify this.
+ #
+ sDetails = self.findInAnyAndReturnRestOfLine([ sVMLog, sResultLog ],
+ 'GIM: HyperV: Guest indicates a fatal condition! P0=');
+ if sDetails is not None:
+ # P0=%#RX64 P1=%#RX64 P2=%#RX64 P3=%#RX64 P4=%#RX64 "
+ sKey = sDetails.split(' ', 1)[0];
+ try: sKey = '0x%08X' % (int(sKey, 16),);
+ except: pass;
+ if sKey in self.asBsodReasons:
+ tReason = ( self.ksBsodCategory, sKey );
+ elif sKey.lower() in self.asBsodReasons: # just in case.
+ tReason = ( self.ksBsodCategory, sKey.lower() );
+ else:
+ self.dprint(u'BSOD "%s" not found in %s;' % (sKey, self.asBsodReasons));
+ tReason = ( self.ksBsodCategory, self.ksBsodAddNew );
+ return oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult, sComment = sDetails.strip());
+
+ fFoundSomething = False;
+
+ #
+ # Look for linux panic.
+ #
+ if sKrnlLog is not None:
+ fRet = self.scanLog([sKrnlLog,], self.katSimpleKernelLogReasons, oCaseFile, oFailedResult.idTestResult);
+ if fRet is True:
+ return fRet;
+ fFoundSomething |= fRet is None;
+
+ #
+ # Loop thru the simple stuff.
+ #
+
+ # Main log.
+ fRet = self.scanLog([sResultLog,], self.katSimpleMainLogReasons, oCaseFile, oFailedResult.idTestResult);
+ if fRet is True:
+ return fRet;
+ fFoundSomething |= fRet is None;
+
+ # VM log.
+ fRet = self.scanLog([sVMLog,], self.katSimpleVmLogReasons, oCaseFile, oFailedResult.idTestResult);
+ if fRet is True:
+ return fRet;
+ fFoundSomething |= fRet is None;
+
+ # Old main + vm log.
+ fRet = self.scanLog([sResultLog, sVMLog], self.katSimpleMainAndVmLogReasonsDeprecated,
+ oCaseFile, oFailedResult.idTestResult);
+ if fRet is True:
+ return fRet;
+ fFoundSomething |= fRet is None;
+
+ # Continue with vga text.
+ if sVgaText:
+ fRet = self.scanLog([sVgaText,], self.katSimpleVgaTextReasons, oCaseFile, oFailedResult.idTestResult);
+ if fRet is True:
+ return fRet;
+ fFoundSomething |= fRet is None;
+
+ # Continue with screen hashes.
+ if sScreenHash is not None:
+ for fStopOnHit, tReason, sHash in self.katSimpleScreenshotHashReasons:
+ if sScreenHash == sHash:
+ oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult);
+ if fStopOnHit:
+ return True;
+ fFoundSomething = True;
+
+ # Check VBoxHardening.log.
+ if sNtHardLog is not None:
+ fRet = self.scanLog([sNtHardLog,], self.katSimpleVBoxHardeningLogReasons, oCaseFile, oFailedResult.idTestResult);
+ if fRet is True:
+ return fRet;
+ fFoundSomething |= fRet is None;
+
+ #
+ # Complicated stuff.
+ #
+ dLogs = {
+ 'sVMLog': sVMLog,
+ 'sNtHardLog': sNtHardLog,
+ 'sScreenHash': sScreenHash,
+ 'sKrnlLog': sKrnlLog,
+ 'sVgaText': sVgaText,
+ 'sInfoText': sInfoText,
+ };
+
+ # info.txt.
+ if sInfoText:
+ for sNeedle, fnHandler in self.katInfoTextHandlers:
+ if sInfoText.find(sNeedle) > 0:
+ (fStop, tReason) = fnHandler(self, oCaseFile, sInfoText, dLogs);
+ if tReason is not None:
+ oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult);
+ if fStop:
+ return True;
+ fFoundSomething = True;
+
+ #
+ # Check for repeated reboots...
+ #
+ if sVMLog is not None:
+ cResets = sVMLog.count('Changing the VM state from \'RUNNING\' to \'RESETTING\'');
+ if cResets > 10:
+ return oCaseFile.noteReasonForId(self.ktReason_Unknown_Reboot_Loop, oFailedResult.idTestResult,
+ sComment = 'Counted %s reboots' % (cResets,));
+
+ return fFoundSomething;
+
+ #
+ # Check if we got any VM or/and kernel logs. Treat them as sets in
+ # case we run multiple VMs here (this is of course ASSUMING they
+ # appear in the order that terminateVmBySession uploads them).
+ #
+ cTimes = 0;
+ sVMLog = None;
+ sNtHardLog = None;
+ sScreenHash = None;
+ sKrnlLog = None;
+ sVgaText = None;
+ sInfoText = None;
+ for oFile in oFailedResult.aoFiles:
+ if oFile.sKind == TestResultFileData.ksKind_LogReleaseVm:
+ if 'VBoxHardening.log' not in oFile.sFile:
+ if sVMLog is not None:
+ if investigateLogSet() is True:
+ return True;
+ cTimes += 1;
+ sInfoText = None;
+ sVgaText = None;
+ sKrnlLog = None;
+ sScreenHash = None;
+ sNtHardLog = None;
+ sVMLog = oCaseFile.getLogFile(oFile);
+ else:
+ sNtHardLog = oCaseFile.getLogFile(oFile);
+ elif oFile.sKind == TestResultFileData.ksKind_LogGuestKernel:
+ sKrnlLog = oCaseFile.getLogFile(oFile);
+ elif oFile.sKind == TestResultFileData.ksKind_InfoVgaText:
+ sVgaText = '\n'.join([sLine.rstrip() for sLine in oCaseFile.getLogFile(oFile).split('\n')]);
+ elif oFile.sKind == TestResultFileData.ksKind_InfoCollection:
+ sInfoText = oCaseFile.getLogFile(oFile);
+ elif oFile.sKind == TestResultFileData.ksKind_ScreenshotFailure:
+ sScreenHash = oCaseFile.getScreenshotSha256(oFile);
+ if sScreenHash is not None:
+ sScreenHash = sScreenHash.lower();
+ self.vprint(u'%s %s' % ( sScreenHash, oFile.sFile,));
+
+ if ( sVMLog is not None \
+ or sNtHardLog is not None \
+ or cTimes == 0) \
+ and investigateLogSet() is True:
+ return True;
+
+ return None;
+
+ def isResultFromVMRun(self, oFailedResult, sResultLog):
+ """
+ Checks if this result and corresponding log snippet looks like a VM run.
+ """
+
+ # Look for startVmEx/ startVmAndConnectToTxsViaTcp and similar output in the log.
+ if sResultLog.find(' startVm') > 0:
+ return True;
+
+ # Any other indicators? No?
+ _ = oFailedResult;
+ return False;
+
+
+ ## Things we search a VBoxSVC log for to figure out why something went bust.
+ katSimpleSvcLogReasons = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( False, ktReason_Unknown_VM_Crash, re.compile(r'Reaper.* exited normally: -1073741819 \(0xc0000005\)') ),
+ ( False, ktReason_Unknown_VM_Crash, re.compile(r'Reaper.* was signalled: 11 \(0xb\)') ), # For VBox < 6.1.
+ ( False, ktReason_Unknown_VM_Crash, re.compile(r'Reaper.* was signalled: SIGABRT.*') ), # Since VBox 7.0.
+ ( False, ktReason_Unknown_VM_Crash, re.compile(r'Reaper.* was signalled: SIGSEGV.*') ),
+ ( False, ktReason_Unknown_VM_Terminated, re.compile(r'Reaper.* was signalled: SIGTERM.*') ),
+ ( False, ktReason_Unknown_VM_Terminated, re.compile(r'Reaper.* was signalled: SIGKILL.*') ),
+ ];
+
+ def investigateSvcLogForVMRun(self, oCaseFile, sSvcLog):
+ """
+ Check the VBoxSVC log for a single VM run.
+ """
+ if sSvcLog:
+ fRet = self.scanLog([sSvcLog,], self.katSimpleSvcLogReasons, oCaseFile, oCaseFile.oTree.idTestResult);
+ if fRet is True or fRet is None:
+ return True;
+ return False;
+
+ def investigateNtHardLogForVMRun(self, oCaseFile):
+ """
+ Check if the hardening log for a single VM run contains VM crash indications.
+ """
+ aoLogFiles = oCaseFile.oTree.getListOfLogFilesByKind(TestResultFileData.ksKind_LogReleaseVm);
+ for oLogFile in aoLogFiles:
+ if oLogFile.sFile.find('VBoxHardening.log') >= 0:
+ sLog = oCaseFile.getLogFile(oLogFile);
+ if sLog.find('Quitting: ExitCode=0xc0000005') >= 0:
+ return oCaseFile.noteReasonForId(self.ktReason_Unknown_VM_Crash, oCaseFile.oTree.idTestResult);
+ return False;
+
+
+ def investigateVBoxVMTest(self, oCaseFile, fSingleVM):
+ """
+ Checks out a VBox VM test.
+
+ This is generic investigation of a test running one or more VMs, like
+ for example a smoke test or a guest installation test.
+
+ The fSingleVM parameter is a hint, which probably won't come in useful.
+ """
+ _ = fSingleVM;
+
+ #
+ # Get a list of test result failures we should be looking into and the main log.
+ #
+ aoFailedResults = oCaseFile.oTree.getListOfFailures();
+ sMainLog = oCaseFile.getMainLog();
+
+ #
+ # There are a set of errors ending up on the top level result record.
+ # Should deal with these first.
+ #
+ if len(aoFailedResults) == 1 and aoFailedResults[0] == oCaseFile.oTree:
+ # Check if we've just got that XPCOM client smoke test shutdown issue. This will currently always
+ # be reported on the top result because vboxinstall.py doesn't add an error for it. It is easy to
+ # ignore other failures in the test if we're not a little bit careful here.
+ if sMainLog.find('vboxinstaller: Exit code: -11 (') > 0:
+ oCaseFile.noteReason(self.ktReason_XPCOM_Exit_Minus_11);
+ return self.caseClosed(oCaseFile);
+
+ # Hang after starting VBoxSVC (e.g. idTestSet=136307258)
+ if self.isThisFollowedByTheseLines(sMainLog, 'oVBoxMgr=<vboxapi.VirtualBoxManager object at',
+ (' Timeout: ', ' Attempting to abort child...',) ):
+ if sMainLog.find('*** glibc detected *** /') > 0:
+ oCaseFile.noteReason(self.ktReason_XPCOM_VBoxSVC_Hang_Plus_Heap_Corruption);
+ else:
+ oCaseFile.noteReason(self.ktReason_XPCOM_VBoxSVC_Hang);
+ return self.caseClosed(oCaseFile);
+
+ # Look for heap corruption without visible hang.
+ if sMainLog.find('*** glibc detected *** /') > 0 \
+ or sMainLog.find("-1073740940") > 0: # STATUS_HEAP_CORRUPTION / 0xc0000374
+ oCaseFile.noteReason(self.ktReason_Unknown_Heap_Corruption);
+ return self.caseClosed(oCaseFile);
+
+ # Out of memory w/ timeout.
+ if sMainLog.find('sErrId=HostMemoryLow') > 0:
+ oCaseFile.noteReason(self.ktReason_Host_HostMemoryLow);
+ return self.caseClosed(oCaseFile);
+
+ # Stale files like vts_rm.exe (windows).
+ offEnd = sMainLog.rfind('*** The test driver exits successfully. ***');
+ if offEnd > 0 and sMainLog.find('[Error 145] The directory is not empty: ', offEnd) > 0:
+ oCaseFile.noteReason(self.ktReason_Ignore_Stale_Files);
+ return self.caseClosed(oCaseFile);
+
+ #
+ # XPCOM screwup
+ #
+ if sMainLog.find('AttributeError: \'NoneType\' object has no attribute \'addObserver\'') > 0:
+ oCaseFile.noteReason(self.ktReason_Buggy_Build_Broken_Build);
+ return self.caseClosed(oCaseFile);
+
+ #
+ # Go thru each failed result.
+ #
+ for oFailedResult in aoFailedResults:
+ self.dprint(u'Looking at test result #%u - %s' % (oFailedResult.idTestResult, oFailedResult.getFullName(),));
+ sResultLog = TestSetData.extractLogSectionElapsed(sMainLog, oFailedResult.tsCreated, oFailedResult.tsElapsed);
+ if oFailedResult.sName == 'Installing VirtualBox':
+ self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = True)
+
+ elif oFailedResult.sName == 'Uninstalling VirtualBox':
+ self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = False)
+
+ elif self.isResultFromVMRun(oFailedResult, sResultLog):
+ self.investigateVMResult(oCaseFile, oFailedResult, sResultLog);
+
+ elif self.isResultFromGATest(oCaseFile, oFailedResult):
+ self.investigateGATest(oCaseFile, oFailedResult, sResultLog);
+
+ elif sResultLog.find('most likely not unique') > 0:
+ oCaseFile.noteReasonForId(self.ktReason_Host_NetworkMisconfiguration, oFailedResult.idTestResult)
+ elif sResultLog.find('Exception: 0x800706be (Call to remote object failed (NS_ERROR_CALL_FAILED))') > 0:
+ oCaseFile.noteReasonForId(self.ktReason_XPCOM_NS_ERROR_CALL_FAILED, oFailedResult.idTestResult);
+
+ elif sResultLog.find('The machine is not mutable (state is ') > 0:
+ self.vprint('Ignoring "machine not mutable" error as it is probably due to an earlier problem');
+ oCaseFile.noteReasonForId(self.ktHarmless, oFailedResult.idTestResult);
+
+ elif sResultLog.find('** error: no action was specified') > 0 \
+ or sResultLog.find('(len(self._asXml, asText))') > 0:
+ oCaseFile.noteReasonForId(self.ktReason_Ignore_Buggy_Test_Driver, oFailedResult.idTestResult);
+
+ else:
+ self.vprint(u'TODO: Cannot place idTestResult=%u - %s' % (oFailedResult.idTestResult, oFailedResult.sName,));
+ self.dprint(u'%s + %s <<\n%s\n<<' % (oFailedResult.tsCreated, oFailedResult.tsElapsed, sResultLog,));
+
+ #
+ # Windows python/com screwup.
+ #
+ if sMainLog.find('ModuleNotFoundError: No module named \'win32com.gen_py') > 0:
+ oCaseFile.noteReason(self.ktReason_Host_win32com_gen_py);
+ return self.caseClosed(oCaseFile);
+
+ #
+ # Check VBoxSVC.log and VBoxHardening.log for VM crashes if inconclusive on single VM runs.
+ #
+ if fSingleVM and len(oCaseFile.dReasonForResultId) < len(aoFailedResults):
+ self.dprint(u'Got %u out of %u - checking VBoxSVC.log...'
+ % (len(oCaseFile.dReasonForResultId), len(aoFailedResults)));
+ if self.investigateSvcLogForVMRun(oCaseFile, oCaseFile.getSvcLog()):
+ return self.caseClosed(oCaseFile);
+ if self.investigateNtHardLogForVMRun(oCaseFile):
+ return self.caseClosed(oCaseFile);
+
+ #
+ # Report home and close the case if we got them all, otherwise log it.
+ #
+ if len(oCaseFile.dReasonForResultId) >= len(aoFailedResults):
+ return self.caseClosed(oCaseFile);
+
+ if oCaseFile.dReasonForResultId:
+ self.vprint(u'TODO: Got %u out of %u - close, but no cigar. :-/'
+ % (len(oCaseFile.dReasonForResultId), len(aoFailedResults)));
+ else:
+ self.vprint(u'XXX: Could not figure out anything at all! :-(');
+ return False;
+
+
+ ## Things we search a main log for to figure out why something in the API test went bust.
+ katSimpleApiMainLogReasons = [
+ # ( Whether to stop on hit, reason tuple, needle text. )
+ ( True, ktReason_Networking_Nonexistent_host_nic,
+ 'rc=E_FAIL text="Nonexistent host networking interface, name \'eth0\' (VERR_INTERNAL_ERROR)"' ),
+ ( False, ktReason_XPCOM_NS_ERROR_CALL_FAILED,
+ 'Exception: 0x800706be (Call to remote object failed (NS_ERROR_CALL_FAILED))' ),
+ ( True, ktReason_API_std_bad_alloc, 'Unexpected exception: std::bad_alloc' ),
+ ( True, ktReason_API_Digest_Mismatch, 'Digest mismatch (VERR_NOT_EQUAL)' ),
+ ( True, ktReason_API_MoveVM_SharingViolation, 'rc=VBOX_E_IPRT_ERROR text="Could not copy the log file ' ),
+ ( True, ktReason_API_MoveVM_InvalidParameter,
+ 'rc=VBOX_E_IPRT_ERROR text="Could not copy the setting file ' ),
+ ( True, ktReason_API_Open_Session_Failed, 'error: failed to open session for' ),
+ ];
+
+ def investigateVBoxApiTest(self, oCaseFile):
+ """
+ Checks out a VBox API test.
+ """
+
+ #
+ # Get a list of test result failures we should be looking into and the main log.
+ #
+ aoFailedResults = oCaseFile.oTree.getListOfFailures();
+ sMainLog = oCaseFile.getMainLog();
+
+ #
+ # Go thru each failed result.
+ #
+ for oFailedResult in aoFailedResults:
+ self.dprint(u'Looking at test result #%u - %s' % (oFailedResult.idTestResult, oFailedResult.getFullName(),));
+ sResultLog = TestSetData.extractLogSectionElapsed(sMainLog, oFailedResult.tsCreated, oFailedResult.tsElapsed);
+ if oFailedResult.sName == 'Installing VirtualBox':
+ self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = True)
+
+ elif oFailedResult.sName == 'Uninstalling VirtualBox':
+ self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = False)
+
+ elif sResultLog.find('Exception: 0x800706be (Call to remote object failed (NS_ERROR_CALL_FAILED))') > 0:
+ oCaseFile.noteReasonForId(self.ktReason_XPCOM_NS_ERROR_CALL_FAILED, oFailedResult.idTestResult);
+
+ else:
+ fFoundSomething = False;
+ for fStopOnHit, tReason, sNeedle in self.katSimpleApiMainLogReasons:
+ if sResultLog.find(sNeedle) > 0:
+ oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult);
+ fFoundSomething = True;
+ if fStopOnHit:
+ break;
+ if fFoundSomething:
+ self.vprint(u'TODO: Cannot place idTestResult=%u - %s' % (oFailedResult.idTestResult, oFailedResult.sName,));
+ self.dprint(u'%s + %s <<\n%s\n<<' % (oFailedResult.tsCreated, oFailedResult.tsElapsed, sResultLog,));
+
+ #
+ # Report home and close the case if we got them all, otherwise log it.
+ #
+ if len(oCaseFile.dReasonForResultId) >= len(aoFailedResults):
+ return self.caseClosed(oCaseFile);
+
+ if oCaseFile.dReasonForResultId:
+ self.vprint(u'TODO: Got %u out of %u - close, but no cigar. :-/'
+ % (len(oCaseFile.dReasonForResultId), len(aoFailedResults)));
+ else:
+ self.vprint(u'XXX: Could not figure out anything at all! :-(');
+ return False;
+
+
+ def reasoningFailures(self):
+ """
+ Guess the reason for failures.
+ """
+ #
+ # Get a list of failed test sets without any assigned failure reason.
+ #
+ cGot = 0;
+ if not self.oConfig.aidTestSets:
+ aoTestSets = self.oTestSetLogic.fetchFailedSetsWithoutReason(cHoursBack = self.oConfig.cHoursBack,
+ tsNow = self.tsNow);
+ else:
+ aoTestSets = [self.oTestSetLogic.getById(idTestSet) for idTestSet in self.oConfig.aidTestSets];
+ for oTestSet in aoTestSets:
+ self.dprint(u'----------------------------------- #%u, status %s -----------------------------------'
+ % ( oTestSet.idTestSet, oTestSet.enmStatus,));
+
+ #
+ # Open a case file and assign it to the right investigator.
+ #
+ (oTree, _ ) = self.oTestResultLogic.fetchResultTree(oTestSet.idTestSet);
+ oBuild = BuildDataEx().initFromDbWithId( self.oDb, oTestSet.idBuild, oTestSet.tsCreated);
+ oTestBox = TestBoxData().initFromDbWithGenId( self.oDb, oTestSet.idGenTestBox);
+ oTestGroup = TestGroupData().initFromDbWithId( self.oDb, oTestSet.idTestGroup, oTestSet.tsCreated);
+ oTestCase = TestCaseDataEx().initFromDbWithGenId( self.oDb, oTestSet.idGenTestCase, oTestSet.tsConfig);
+
+ oCaseFile = VirtualTestSheriffCaseFile(self, oTestSet, oTree, oBuild, oTestBox, oTestGroup, oTestCase);
+
+ if oTestSet.enmStatus == TestSetData.ksTestStatus_BadTestBox:
+ self.dprint(u'investigateBadTestBox is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateBadTestBox(oCaseFile);
+
+ elif oCaseFile.isVBoxUnitTest():
+ self.dprint(u'investigateVBoxUnitTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxUnitTest(oCaseFile);
+
+ elif oCaseFile.isVBoxInstallTest() or oCaseFile.isVBoxUnattendedInstallTest():
+ self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = True);
+
+ elif oCaseFile.isVBoxUSBTest():
+ self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = True);
+
+ elif oCaseFile.isVBoxStorageTest():
+ self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = True);
+
+ elif oCaseFile.isVBoxGAsTest():
+ self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = True);
+
+ elif oCaseFile.isVBoxAPITest():
+ self.dprint(u'investigateVBoxApiTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxApiTest(oCaseFile);
+
+ elif oCaseFile.isVBoxBenchmarkTest():
+ self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = False);
+
+ elif oCaseFile.isVBoxSmokeTest():
+ self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = False);
+
+ elif oCaseFile.isVBoxSerialTest():
+ self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,));
+ fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = False);
+
+ else:
+ self.vprint(u'reasoningFailures: Unable to classify test set: %s' % (oCaseFile.sLongName,));
+ fRc = False;
+ cGot += fRc is True;
+
+ self.vprint(u'reasoningFailures: Got %u out of %u' % (cGot, len(aoTestSets), ));
+ return 0;
+
+
+ def main(self):
+ """
+ The 'main' function.
+ Return exit code (0, 1, etc).
+ """
+ # Database stuff.
+ self.oDb = TMDatabaseConnection()
+ self.oTestResultLogic = TestResultLogic(self.oDb);
+ self.oTestSetLogic = TestSetLogic(self.oDb);
+ self.oFailureReasonLogic = FailureReasonLogic(self.oDb);
+ self.oTestResultFailureLogic = TestResultFailureLogic(self.oDb);
+ self.asBsodReasons = self.oFailureReasonLogic.fetchForSheriffByNamedCategory(self.ksBsodCategory);
+ self.asUnitTestReasons = self.oFailureReasonLogic.fetchForSheriffByNamedCategory(self.ksUnitTestCategory);
+
+ # Get a fix on our 'now' before we do anything..
+ self.oDb.execute('SELECT CURRENT_TIMESTAMP - interval \'%s hours\'', (self.oConfig.cStartHoursAgo,));
+ self.tsNow = self.oDb.fetchOne();
+
+ # If we're suppost to commit anything we need to get our user ID.
+ rcExit = 0;
+ if self.oConfig.fRealRun:
+ self.oLogin = UserAccountLogic(self.oDb).tryFetchAccountByLoginName(VirtualTestSheriff.ksLoginName);
+ if self.oLogin is None:
+ rcExit = self.eprint('Cannot find my user account "%s"!' % (VirtualTestSheriff.ksLoginName,));
+ else:
+ self.uidSelf = self.oLogin.uid;
+
+ #
+ # Do the stuff.
+ #
+ if rcExit == 0:
+ rcExit = self.selfCheck();
+ if rcExit == 0:
+ rcExit = self.badTestBoxManagement();
+ rcExit2 = self.reasoningFailures();
+ if rcExit == 0:
+ rcExit = rcExit2;
+ # Redo the bad testbox management after failure reasons have been assigned (got timing issues).
+ if rcExit == 0:
+ rcExit = self.badTestBoxManagement();
+
+ # Cleanup.
+ self.oFailureReasonLogic = None;
+ self.oTestResultFailureLogic = None;
+ self.oTestSetLogic = None;
+ self.oTestResultLogic = None;
+ self.oDb.close();
+ self.oDb = None;
+ if self.oLogFile is not None:
+ self.oLogFile.close();
+ self.oLogFile = None;
+ return rcExit;
+
+if __name__ == '__main__':
+ sys.exit(VirtualTestSheriff().main());
diff --git a/src/VBox/ValidationKit/testmanager/cgi/Makefile.kmk b/src/VBox/ValidationKit/testmanager/cgi/Makefile.kmk
new file mode 100644
index 00000000..74d882cc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/Makefile.kmk
@@ -0,0 +1,46 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+SUB_DEPTH = ../../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+
+VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py)
+
+$(evalcall def_vbox_validationkit_process_python_sources)
+$(evalcall def_vbox_validationkit_process_js_sources)
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/testmanager/cgi/admin.py b/src/VBox/ValidationKit/testmanager/cgi/admin.py
new file mode 100755
index 00000000..b8c10f76
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/admin.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: admin.py $
+
+"""
+CGI - Administrator Web-UI.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os
+import sys
+
+# Only the main script needs to modify the path.
+g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksValidationKitDir);
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core.webservergluecgi import WebServerGlueCgi;
+from testmanager.webui.wuiadmin import WuiAdmin;
+
+def main():
+ """
+ Main function a la C/C++. Returns exit code.
+ """
+
+ oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = True);
+ try:
+ oWui = WuiAdmin(oSrvGlue);
+ oWui.dispatchRequest();
+ oSrvGlue.flush();
+ except Exception as oXcpt:
+ return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info());
+
+ return 0;
+
+if __name__ == '__main__':
+ if config.g_kfProfileAdmin:
+ from testmanager.debug import cgiprofiling;
+ sys.exit(cgiprofiling.profileIt(main));
+ else:
+ sys.exit(main());
+
diff --git a/src/VBox/ValidationKit/testmanager/cgi/debuginfo.py b/src/VBox/ValidationKit/testmanager/cgi/debuginfo.py
new file mode 100755
index 00000000..a2663436
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/debuginfo.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: debuginfo.py $
+
+"""
+CGI - Debug Info Page.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os
+import sys
+
+# Only the main script needs to modify the path.
+g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksValidationKitDir);
+
+# Validation Kit imports.
+from testmanager.core.webservergluecgi import WebServerGlueCgi;
+
+
+def main():
+ """
+ Main function a la C/C++. Returns exit code.
+ """
+
+ oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = True);
+ try:
+ oSrvGlue.debugInfoPage();
+ oSrvGlue.flush();
+ except Exception as oXcpt:
+ return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info());
+
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(main());
+
diff --git a/src/VBox/ValidationKit/testmanager/cgi/index.py b/src/VBox/ValidationKit/testmanager/cgi/index.py
new file mode 100755
index 00000000..b9b546c2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/index.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: index.py $
+
+"""
+CGI - Web UI - Main (index) page.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os
+import sys
+
+# Only the main script needs to modify the path.
+g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksValidationKitDir);
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core.webservergluecgi import WebServerGlueCgi;
+from testmanager.webui.wuimain import WuiMain;
+
+
+def main():
+ """
+ Main function a la C/C++. Returns exit code.
+ """
+
+ oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False);
+ try:
+ oWui = WuiMain(oSrvGlue);
+ oWui.dispatchRequest();
+ oSrvGlue.flush();
+ except Exception as oXcpt:
+ return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info());
+
+ return 0;
+
+if __name__ == '__main__':
+ if config.g_kfProfileIndex:
+ from testmanager.debug import cgiprofiling;
+ sys.exit(cgiprofiling.profileIt(main));
+ else:
+ sys.exit(main());
+
diff --git a/src/VBox/ValidationKit/testmanager/cgi/logout.py b/src/VBox/ValidationKit/testmanager/cgi/logout.py
new file mode 100755
index 00000000..109bfa86
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/logout.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: logout.py $
+
+"""
+VirtualBox Validation Kit - CGI - Log out page.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os
+import sys
+
+# Only the main script needs to modify the path.
+g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksValidationKitDir);
+
+# Validation Kit imports.
+from testmanager.core.webservergluecgi import WebServerGlueCgi
+
+
+def main():
+ """
+ Main function a la C/C++. Returns exit code.
+ """
+
+ oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = True)
+ sUser = oSrvGlue.getLoginName()
+ if sUser not in (oSrvGlue.ksUnknownUser, 'logout'):
+ oSrvGlue.write('<p>Broken apache config!\n'
+ 'The logout.py script should be configured with .htaccess-logout and require user logout!</p>')
+ else:
+ oSrvGlue.write('<p>Successfully logged out!</p>')
+ oSrvGlue.write('<p><a href="%sadmin.py">Log in</a> under another user name.</p>' %
+ (oSrvGlue.getBaseUrl(),))
+
+
+ oSrvGlue.write('<hr/><p>debug info:</p>')
+ oSrvGlue.debugInfoPage()
+ oSrvGlue.flush()
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main())
+
diff --git a/src/VBox/ValidationKit/testmanager/cgi/logout2.py b/src/VBox/ValidationKit/testmanager/cgi/logout2.py
new file mode 100755
index 00000000..1ab24cbc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/logout2.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: logout2.py $
+
+"""
+VirtualBox Validation Kit - CGI - Log out page for Safari.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os
+import sys
+
+# Only the main script needs to modify the path.
+g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksValidationKitDir);
+
+# Validation Kit imports.
+from testmanager.core.webservergluecgi import WebServerGlueCgi;
+
+
+def main():
+ """
+ Main function a la C/C++. Returns exit code.
+ """
+
+ oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = True);
+ sUserAgent = oSrvGlue.getUserAgent();
+ oSrvGlue.setHeaderField('Status', '401 Unauthorized to access the document');
+ oSrvGlue.setHeaderField('WWW-authenticate', 'Basic realm="Test Manager"');
+ if sUserAgent.startswith('Mozilla/') and sUserAgent.find('AppleWebKit/') > 0:
+ oSrvGlue.write('<p>Attempting to log out an Apple browser...</p>');
+ else:
+ oSrvGlue.write('<p>Sorry, not sure this will work...</p>');
+ oSrvGlue.write('<p>User-Agent:' + sUserAgent + '</p>');
+
+ oSrvGlue.write('<p><a href="%sadmin.py">Log in</a> under another user name.</p>' %
+ (oSrvGlue.getBaseUrl(),))
+
+ oSrvGlue.write('<hr/><p>debug info:</p>');
+ oSrvGlue.debugInfoPage();
+ oSrvGlue.flush();
+
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(main());
+
diff --git a/src/VBox/ValidationKit/testmanager/cgi/rest.py b/src/VBox/ValidationKit/testmanager/cgi/rest.py
new file mode 100755
index 00000000..89a5238c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/rest.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: rest.py $
+
+"""
+CGI - REST - sPath=path variant.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os
+import sys
+
+# Only the main script needs to modify the path.
+g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksValidationKitDir);
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core.webservergluecgi import WebServerGlueCgi;
+from testmanager.core.restdispatcher import RestMain, RestDispException;
+
+
+def main():
+ """
+ Main function a la C/C++. Returns exit code.
+ """
+
+ oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False);
+ try:
+ oMain = RestMain(oSrvGlue);
+ oMain.dispatchRequest();
+ oSrvGlue.flush();
+ except RestDispException as oXcpt:
+ oSrvGlue.setStatus(oXcpt.iStatus);
+ oSrvGlue.setHeaderField('tm-error-message', str(oXcpt));
+ oSrvGlue.write('error: ' + str(oXcpt));
+ oSrvGlue.flush();
+ except Exception as oXcpt:
+ return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),),
+ sys.exc_info(),
+ config.g_ksTestBoxDispXpctLog);
+
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(main());
+
diff --git a/src/VBox/ValidationKit/testmanager/cgi/status.py b/src/VBox/ValidationKit/testmanager/cgi/status.py
new file mode 100755
index 00000000..39c8af03
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/status.py
@@ -0,0 +1,519 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: status.py $
+
+"""
+CGI - Administrator Web-UI.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os
+import sys
+
+# Only the main script needs to modify the path.
+g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksValidationKitDir);
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core.webservergluecgi import WebServerGlueCgi;
+
+from common import constants;
+from testmanager.core.base import TMExceptionBase;
+from testmanager.core.db import TMDatabaseConnection;
+
+
+
+def timeDeltaToHours(oTimeDelta):
+ return oTimeDelta.days * 24 + oTimeDelta.seconds // 3600
+
+
+def testbox_data_processing(oDb):
+ testboxes_dict = {}
+ while True:
+ line = oDb.fetchOne();
+ if line is None:
+ break;
+ testbox_name = line[0]
+ test_result = line[1]
+ oTimeDeltaSinceStarted = line[2]
+ test_box_os = line[3]
+ test_sched_group = line[4]
+
+ # idle testboxes might have an assigned testsets, skipping them
+ if test_result not in g_kdTestStatuses:
+ continue
+
+ testboxes_dict = dict_update(testboxes_dict, testbox_name, test_result)
+
+ if "testbox_os" not in testboxes_dict[testbox_name]:
+ testboxes_dict[testbox_name].update({"testbox_os": test_box_os})
+
+ if "sched_group" not in testboxes_dict[testbox_name]:
+ testboxes_dict[testbox_name].update({"sched_group": test_sched_group})
+ elif test_sched_group not in testboxes_dict[testbox_name]["sched_group"]:
+ testboxes_dict[testbox_name]["sched_group"] += "," + test_sched_group
+
+ if test_result == "running":
+ testboxes_dict[testbox_name].update({"hours_running": timeDeltaToHours(oTimeDeltaSinceStarted)})
+
+ return testboxes_dict;
+
+
+def os_results_separating(vb_dict, test_name, testbox_os, test_result):
+ if testbox_os == "linux":
+ dict_update(vb_dict, test_name + " / linux", test_result)
+ elif testbox_os == "win":
+ dict_update(vb_dict, test_name + " / windows", test_result)
+ elif testbox_os == "darwin":
+ dict_update(vb_dict, test_name + " / darwin", test_result)
+ elif testbox_os == "solaris":
+ dict_update(vb_dict, test_name + " / solaris", test_result)
+ else:
+ dict_update(vb_dict, test_name + " / other", test_result)
+
+
+# const/immutable.
+g_kdTestStatuses = {
+ 'running': 0,
+ 'success': 0,
+ 'skipped': 0,
+ 'bad-testbox': 0,
+ 'aborted': 0,
+ 'failure': 0,
+ 'timed-out': 0,
+ 'rebooted': 0,
+}
+
+def dict_update(target_dict, key_name, test_result):
+ if key_name not in target_dict:
+ target_dict.update({key_name: g_kdTestStatuses.copy()})
+ if test_result in g_kdTestStatuses:
+ target_dict[key_name][test_result] += 1
+ return target_dict
+
+
+def formatDataEntry(sKey, dEntry):
+ # There are variations in the first and second "columns".
+ if "hours_running" in dEntry:
+ sRet = "%s;%s;%s | running: %s;%s" \
+ % (sKey, dEntry["testbox_os"], dEntry["sched_group"], dEntry["running"], dEntry["hours_running"]);
+ else:
+ if "testbox_os" in dEntry:
+ sRet = "%s;%s;%s" % (sKey, dEntry["testbox_os"], dEntry["sched_group"],);
+ else:
+ sRet = sKey;
+ sRet += " | running: %s" % (dEntry["running"],)
+
+ # The rest is currently identical:
+ sRet += " | success: %s | skipped: %s | bad-testbox: %s | aborted: %s | failure: %s | timed-out: %s | rebooted: %s | \n" \
+ % (dEntry["success"], dEntry["skipped"], dEntry["bad-testbox"], dEntry["aborted"],
+ dEntry["failure"], dEntry["timed-out"], dEntry["rebooted"],);
+ return sRet;
+
+
+def format_data(dData, fSorted):
+ sRet = "";
+ if not fSorted:
+ for sKey in dData:
+ sRet += formatDataEntry(sKey, dData[sKey]);
+ else:
+ for sKey in sorted(dData.keys()):
+ sRet += formatDataEntry(sKey, dData[sKey]);
+ return sRet;
+
+######
+
+class StatusDispatcherException(TMExceptionBase):
+ """
+ Exception class for TestBoxController.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class StatusDispatcher(object): # pylint: disable=too-few-public-methods
+ """
+ Status dispatcher class.
+ """
+
+
+ def __init__(self, oSrvGlue):
+ """
+ Won't raise exceptions.
+ """
+ self._oSrvGlue = oSrvGlue;
+ self._sAction = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._dParams = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._asCheckedParams = [];
+ self._dActions = \
+ {
+ 'MagicMirrorTestResults': self._actionMagicMirrorTestResults,
+ 'MagicMirrorTestBoxes': self._actionMagicMirrorTestBoxes,
+ };
+
+ def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None):
+ """
+ Gets a string parameter (stripped).
+
+ Raises exception if not found and no default is provided, or if the
+ value isn't found in asValidValues.
+ """
+ if sName not in self._dParams:
+ if sDefValue is None:
+ raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName));
+ return sDefValue;
+ sValue = self._dParams[sName];
+ if fStrip:
+ sValue = sValue.strip();
+
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ if asValidValues is not None and sValue not in asValidValues:
+ raise StatusDispatcherException('%s parameter %s value "%s" not in %s '
+ % (self._sAction, sName, sValue, asValidValues));
+ return sValue;
+
+ def _getIntParam(self, sName, iMin = None, iMax = None, iDefValue = None):
+ """
+ Gets a string parameter.
+ Raises exception if not found, not a valid integer, or if the value
+ isn't in the range defined by iMin and iMax.
+ """
+ if sName not in self._dParams:
+ if iDefValue is None:
+ raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName));
+ return iDefValue;
+ sValue = self._dParams[sName];
+ try:
+ iValue = int(sValue, 0);
+ except:
+ raise StatusDispatcherException('%s parameter %s value "%s" cannot be convert to an integer'
+ % (self._sAction, sName, sValue));
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ if (iMin is not None and iValue < iMin) \
+ or (iMax is not None and iValue > iMax):
+ raise StatusDispatcherException('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sAction, sName, iValue, iMin, iMax));
+ return iValue;
+
+ def _getBoolParam(self, sName, fDefValue = None):
+ """
+ Gets a boolean parameter.
+
+ Raises exception if not found and no default is provided, or if not a
+ valid boolean.
+ """
+ sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue));
+ return sValue in ('True', 'true', '1',);
+
+ def _checkForUnknownParameters(self):
+ """
+ Check if we've handled all parameters, raises exception if anything
+ unknown was found.
+ """
+
+ if len(self._asCheckedParams) != len(self._dParams):
+ sUnknownParams = '';
+ for sKey in self._dParams:
+ if sKey not in self._asCheckedParams:
+ sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey];
+ raise StatusDispatcherException('Unknown parameters: ' + sUnknownParams);
+
+ return True;
+
+ def _connectToDb(self):
+ """
+ Connects to the database.
+
+ Returns (TMDatabaseConnection, (more later perhaps) ) on success.
+ Returns (None, ) on failure after sending the box an appropriate response.
+ May raise exception on DB error.
+ """
+ return (TMDatabaseConnection(self._oSrvGlue.dprint),);
+
+ def _actionMagicMirrorTestBoxes(self):
+ """
+ Produces test result status for the magic mirror dashboard
+ """
+
+ #
+ # Parse arguments and connect to the database.
+ #
+ cHoursBack = self._getIntParam('cHours', 1, 24*14, 12);
+ fSorted = self._getBoolParam('fSorted', False);
+ self._checkForUnknownParameters();
+
+ #
+ # Get the data.
+ #
+ # Note! We're not joining on TestBoxesWithStrings.idTestBox =
+ # TestSets.idGenTestBox here because of indexes. This is
+ # also more consistent with the rest of the query.
+ # Note! The original SQL is slow because of the 'OR TestSets.tsDone'
+ # part, using AND and UNION is significatly faster because
+ # it matches the TestSetsGraphBoxIdx (index).
+ #
+ (oDb,) = self._connectToDb();
+ if oDb is None:
+ return False;
+
+ #
+ # some comments regarding select below:
+ # first part is about fetching all finished tests for last cHoursBack hours
+ # second part is fetching all tests which isn't done
+ # both old (running more than cHoursBack) and fresh (less than cHoursBack) ones
+ # 'cause we want to know if there's a hanging tests together with currently running
+ #
+ # there's also testsets without status at all, likely because disabled testboxes still have an assigned testsets
+ #
+ oDb.execute('''
+( SELECT TestBoxesWithStrings.sName,
+ TestSets.enmStatus,
+ CURRENT_TIMESTAMP - TestSets.tsCreated,
+ TestBoxesWithStrings.sOS,
+ SchedGroupNames.sSchedGroupNames
+ FROM (
+ SELECT TestBoxesInSchedGroups.idTestBox AS idTestBox,
+ STRING_AGG(SchedGroups.sName, ',') AS sSchedGroupNames
+ FROM TestBoxesInSchedGroups
+ INNER JOIN SchedGroups
+ ON SchedGroups.idSchedGroup = TestBoxesInSchedGroups.idSchedGroup
+ WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP
+ AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP
+ GROUP BY TestBoxesInSchedGroups.idTestBox
+ ) AS SchedGroupNames,
+ TestBoxesWithStrings
+ LEFT OUTER JOIN TestSets
+ ON TestSets.idTestBox = TestBoxesWithStrings.idTestBox
+ AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
+ AND TestSets.tsDone IS NOT NULL
+ WHERE TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
+ AND SchedGroupNames.idTestBox = TestBoxesWithStrings.idTestBox
+) UNION (
+ SELECT TestBoxesWithStrings.sName,
+ TestSets.enmStatus,
+ CURRENT_TIMESTAMP - TestSets.tsCreated,
+ TestBoxesWithStrings.sOS,
+ SchedGroupNames.sSchedGroupNames
+ FROM (
+ SELECT TestBoxesInSchedGroups.idTestBox AS idTestBox,
+ STRING_AGG(SchedGroups.sName, ',') AS sSchedGroupNames
+ FROM TestBoxesInSchedGroups
+ INNER JOIN SchedGroups
+ ON SchedGroups.idSchedGroup = TestBoxesInSchedGroups.idSchedGroup
+ WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP
+ AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP
+ GROUP BY TestBoxesInSchedGroups.idTestBox
+ ) AS SchedGroupNames,
+ TestBoxesWithStrings
+ LEFT OUTER JOIN TestSets
+ ON TestSets.idTestBox = TestBoxesWithStrings.idTestBox
+ AND TestSets.tsDone IS NULL
+ WHERE TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
+ AND SchedGroupNames.idTestBox = TestBoxesWithStrings.idTestBox
+)
+''', (cHoursBack, cHoursBack,));
+
+
+ #
+ # Process, format and output data.
+ #
+ dResult = testbox_data_processing(oDb);
+ self._oSrvGlue.setContentType('text/plain');
+ self._oSrvGlue.write(format_data(dResult, fSorted));
+
+ return True;
+
+ def _actionMagicMirrorTestResults(self):
+ """
+ Produces test result status for the magic mirror dashboard
+ """
+
+ #
+ # Parse arguments and connect to the database.
+ #
+ sBranch = self._getStringParam('sBranch');
+ cHoursBack = self._getIntParam('cHours', 1, 24*14, 6); ## @todo why 6 hours here and 12 for test boxes?
+ fSorted = self._getBoolParam('fSorted', False);
+ self._checkForUnknownParameters();
+
+ #
+ # Get the data.
+ #
+ # Note! These queries should be joining TestBoxesWithStrings and TestSets
+ # on idGenTestBox rather than on idTestBox and tsExpire=inf, but
+ # we don't have any index matching those. So, we'll ignore tests
+ # performed by deleted testboxes for the present as that doesn't
+ # happen often and we want the ~1000x speedup.
+ #
+ (oDb,) = self._connectToDb();
+ if oDb is None:
+ return False;
+
+ if sBranch == 'all':
+ oDb.execute('''
+SELECT TestSets.enmStatus,
+ TestCases.sName,
+ TestBoxesWithStrings.sOS
+FROM TestSets
+INNER JOIN TestCases
+ ON TestCases.idGenTestCase = TestSets.idGenTestCase
+INNER JOIN TestBoxesWithStrings
+ ON TestBoxesWithStrings.idTestBox = TestSets.idTestBox
+ AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
+WHERE TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
+''', (cHoursBack,));
+ else:
+ oDb.execute('''
+SELECT TestSets.enmStatus,
+ TestCases.sName,
+ TestBoxesWithStrings.sOS
+FROM TestSets
+INNER JOIN BuildCategories
+ ON BuildCategories.idBuildCategory = TestSets.idBuildCategory
+ AND BuildCategories.sBranch = %s
+INNER JOIN TestCases
+ ON TestCases.idGenTestCase = TestSets.idGenTestCase
+INNER JOIN TestBoxesWithStrings
+ ON TestBoxesWithStrings.idTestBox = TestSets.idTestBox
+ AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
+WHERE TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
+''', (sBranch, cHoursBack,));
+
+ # Process the data
+ dResult = {};
+ while True:
+ aoRow = oDb.fetchOne();
+ if aoRow is None:
+ break;
+ os_results_separating(dResult, aoRow[1], aoRow[2], aoRow[0]) # save all test results
+
+ # Format and output it.
+ self._oSrvGlue.setContentType('text/plain');
+ self._oSrvGlue.write(format_data(dResult, fSorted));
+
+ return True;
+
+ def _getStandardParams(self, dParams):
+ """
+ Gets the standard parameters and validates them.
+
+ The parameters are returned as a tuple: sAction, (more later, maybe)
+ Note! the sTextBoxId can be None if it's a SIGNON request.
+
+ Raises StatusDispatcherException on invalid input.
+ """
+ #
+ # Get the action parameter and validate it.
+ #
+ if constants.tbreq.ALL_PARAM_ACTION not in dParams:
+ raise StatusDispatcherException('No "%s" parameter in request (params: %s)'
+ % (constants.tbreq.ALL_PARAM_ACTION, dParams,));
+ sAction = dParams[constants.tbreq.ALL_PARAM_ACTION];
+
+ if sAction not in self._dActions:
+ raise StatusDispatcherException('Unknown action "%s" in request (params: %s; action: %s)'
+ % (sAction, dParams, self._dActions));
+ #
+ # Update the list of checked parameters.
+ #
+ self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_ACTION,]);
+
+ return (sAction,);
+
+ def dispatchRequest(self):
+ """
+ Dispatches the incoming request.
+
+ Will raise StatusDispatcherException on failure.
+ """
+
+ #
+ # Must be a GET request.
+ #
+ try:
+ sMethod = self._oSrvGlue.getMethod();
+ except Exception as oXcpt:
+ raise StatusDispatcherException('Error retriving request method: %s' % (oXcpt,));
+ if sMethod != 'GET':
+ raise StatusDispatcherException('Error expected POST request not "%s"' % (sMethod,));
+
+ #
+ # Get the parameters and checks for duplicates.
+ #
+ try:
+ dParams = self._oSrvGlue.getParameters();
+ except Exception as oXcpt:
+ raise StatusDispatcherException('Error retriving parameters: %s' % (oXcpt,));
+ for sKey in dParams.keys():
+ if len(dParams[sKey]) > 1:
+ raise StatusDispatcherException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey]));
+ dParams[sKey] = dParams[sKey][0];
+ self._dParams = dParams;
+
+ #
+ # Get+validate the standard action parameters and dispatch the request.
+ #
+ (self._sAction, ) = self._getStandardParams(dParams);
+ return self._dActions[self._sAction]();
+
+
+def main():
+ """
+ Main function a la C/C++. Returns exit code.
+ """
+
+ oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False);
+ try:
+ oDisp = StatusDispatcher(oSrvGlue);
+ oDisp.dispatchRequest();
+ oSrvGlue.flush();
+ except Exception as oXcpt:
+ return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info());
+
+ return 0;
+
+if __name__ == '__main__':
+ if config.g_kfProfileAdmin:
+ from testmanager.debug import cgiprofiling;
+ sys.exit(cgiprofiling.profileIt(main));
+ else:
+ sys.exit(main());
+
diff --git a/src/VBox/ValidationKit/testmanager/cgi/testboxdisp.py b/src/VBox/ValidationKit/testmanager/cgi/testboxdisp.py
new file mode 100755
index 00000000..c5c704bb
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/cgi/testboxdisp.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: testboxdisp.py $
+
+"""
+CGI - TestBox Interaction (see testboxscript or the other party).
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os
+import sys
+
+# Only the main script needs to modify the path.
+g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksValidationKitDir);
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core.webservergluecgi import WebServerGlueCgi;
+from testmanager.core.testboxcontroller import TestBoxController;
+
+
+def main():
+ """
+ Main function a la C/C++. Returns exit code.
+ """
+
+ oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False);
+ oCtrl = TestBoxController(oSrvGlue);
+ try:
+ oCtrl.dispatchRequest()
+ oSrvGlue.flush();
+ except Exception as oXcpt:
+ return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),),
+ sys.exc_info(),
+ config.g_ksTestBoxDispXpctLog);
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(main());
+
diff --git a/src/VBox/ValidationKit/testmanager/config.py b/src/VBox/ValidationKit/testmanager/config.py
new file mode 100644
index 00000000..b4ef94cc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/config.py
@@ -0,0 +1,261 @@
+# -*- coding: utf-8 -*-
+# $Id: config.py $
+
+"""
+Test Manager Configuration.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+import os;
+
+## Test Manager version string.
+g_ksVersion = 'v0.1.0';
+## Test Manager revision string.
+g_ksRevision = ('$Revision: 155244 $')[11:-2];
+
+## Enable VBox specific stuff.
+g_kfVBoxSpecific = True;
+
+
+## @name Used by the TMDatabaseConnection class.
+# @{
+g_ksDatabaseName = 'testmanager';
+g_ksDatabaseAddress = None;
+g_ksDatabasePort = None;
+g_ksDatabaseUser = 'postgres';
+g_ksDatabasePassword = '';
+## @}
+
+
+## @name User handling.
+## @{
+
+## Whether login names are case insensitive (True) or case sensitive (False).
+## @note Implemented by inserting lower case names into DB and lower case
+## bind variables in WHERE clauses.
+g_kfLoginNameCaseInsensitive = True;
+
+## @}
+
+
+## @name File locations
+## @{
+
+## The TestManager directory.
+g_ksTestManagerDir = os.path.dirname(os.path.abspath(__file__));
+## The Validation Kit directory.
+g_ksValidationKitDir = os.path.dirname(g_ksTestManagerDir);
+## The TestManager htdoc directory.
+g_ksTmHtDocDir = os.path.join(g_ksTestManagerDir, 'htdocs');
+## The TestManager download directory (under htdoc somewhere), for validationkit zips.
+g_ksTmDownloadDir = os.path.join(g_ksTmHtDocDir, 'download');
+## The base URL relative path of the TM download directory (g_ksTmDownloadDir).
+g_ksTmDownloadBaseUrlRel = 'htdocs/downloads';
+## The root of the file area (referred to as TM_FILE_DIR in database docs).
+g_ksFileAreaRootDir = '/var/tmp/testmanager'
+## The root of the file area with the zip files (best put on a big storage server).
+g_ksZipFileAreaRootDir = '/var/tmp/testmanager2'
+## URL prefix for trac log viewer.
+g_ksTracLogUrlPrefix = 'https://linserv.de.oracle.com/vbox/log/'
+## URL prefix for trac log viewer.
+g_ksTracChangsetUrlFmt = 'https://linserv.de.oracle.com/%(sRepository)s/changeset/%(iRevision)s'
+## URL prefix for unprefixed build logs.
+g_ksBuildLogUrlPrefix = ''
+## URL prefix for unprefixed build binaries.
+g_ksBuildBinUrlPrefix = '/builds/'
+## The local path prefix for unprefixed build binaries. (Host file system, not web server.)
+g_ksBuildBinRootDir = '/mnt/builds/'
+## File on the build binary share that can be used to check that it's mounted.
+g_ksBuildBinRootFile = 'builds.txt'
+## Template for paratial database dump output files. One argument: UID
+g_ksTmDbDumpOutFileTmpl = '/var/tmp/tm-partial-db-dump-for-%u.zip'
+## Template for paratial database dump temporary files. One argument: UID
+g_ksTmDbDumpTmpFileTmpl = '/var/tmp/tm-partial-db-dump-for-%u.pgtxt'
+## @}
+
+
+## @name Scheduling parameters
+## @{
+
+## The time to wait for a gang to gather (in seconds).
+g_kcSecGangGathering = 600;
+## The max time allowed to spend looking for a new task (in seconds).
+g_kcSecMaxNewTask = 60;
+## Minimum time since last task started.
+g_kcSecMinSinceLastTask = 120; # (2 min)
+## Minimum time since last failed task.
+g_kcSecMinSinceLastFailedTask = 180; # (3 min)
+
+## @}
+
+
+
+## @name Test result limits.
+## In general, we will fail the test when reached and stop accepting further results.
+## @{
+
+## The max number of test results per test set.
+g_kcMaxTestResultsPerTS = 4096;
+## The max number of test results (children) per test result.
+g_kcMaxTestResultsPerTR = 512;
+## The max number of test result values per test set.
+g_kcMaxTestValuesPerTS = 4096;
+## The max number of test result values per test result.
+g_kcMaxTestValuesPerTR = 256;
+## The max number of test result message per test result.
+g_kcMaxTestMsgsPerTR = 4;
+## The max test result nesting depth.
+g_kcMaxTestResultDepth = 10;
+
+## The max length of a test result name.
+g_kcchMaxTestResultName = 64;
+## The max length of a test result value name.
+g_kcchMaxTestValueName = 56;
+## The max length of a test result message.
+g_kcchMaxTestMsg = 128;
+
+## The max size of the main log file.
+g_kcMbMaxMainLog = 32;
+## The max size of an uploaded file (individual).
+g_kcMbMaxUploadSingle = 150;
+## The max size of all uploaded file.
+g_kcMbMaxUploadTotal = 200;
+## The max number of files that can be uploaded.
+g_kcMaxUploads = 256;
+## @}
+
+
+## @name Bug Trackers and VCS reference tags.
+## @{
+class BugTrackerConfig(object):
+ """ Bug tracker config """
+ def __init__(self, sDbId, sName, sBugUrl, asCommitTags):
+ assert len(sDbId) == 4;
+ self.sDbId = sDbId;
+ self.sName = sName;
+ self.sBugUrl = sBugUrl;
+ self.asCommitTags = asCommitTags;
+
+## The key is the database table
+g_kdBugTrackers = {
+ 'xtrk': BugTrackerConfig('xtrk', 'xTracker', 'https://linserv.de.oracle.com/vbox/xTracker/index.php?bug=',
+ ['bugref:', '@bugref{', 'bugef:', 'bugrf:', ], ),
+ 'bgdb': BugTrackerConfig('bgdb', 'BugDB', 'https://bug.oraclecorp.com/pls/bug/webbug_edit.edit_info_top?rptno=',
+ ['bugdbref:', '@bugdbref{', 'bugdb:', ], ),
+ 'vorg': BugTrackerConfig('vorg', 'External Trac', 'https://www.virtualbox.org/ticket/',
+ ['ticketref:', '@ticketref{', 'ticket:', ], ),
+};
+## @}
+
+
+
+## @name Virtual Sheriff email alerts
+## @{
+
+## SMTP server host name.
+g_ksSmtpHost = 'internal-mail-router.oracle.com';
+## SMTP server port number.
+g_kcSmtpPort = 25;
+## Default email 'From' for email alert.
+g_ksAlertFrom = 'vsheriff@oracle.com';
+## Subject for email alert.
+g_ksAlertSubject = 'Virtual Test Sheriff Alert';
+## List of users to send alerts.
+g_asAlertList = ['alertuser1', 'alertuser2'];
+## iLOM password.
+g_ksLomPassword = 'put_your_ILOM_password_here_if_applicable';
+
+## @}
+
+
+## @name Partial Database Dump
+## @{
+
+## Minimum number of day. Set higher than g_kcTmDbDumpMaxDays to disable.
+g_kcTmDbDumpMinDays = 1;
+## Maximum number of day. Keep low - consider space and runtime.
+g_kcTmDbDumpMaxDays = 31;
+## The default number of days.
+g_kcTmDbDumpDefaultDays = 14;
+## @}
+
+
+## @name Debug Features
+## @{
+
+## Enables extra DB exception information.
+g_kfDebugDbXcpt = True;
+
+## Where to write the glue debug.
+# None indicates apache error log, string indicates a file.
+#g_ksSrvGlueDebugLogDst = '/tmp/testmanager-srv-glue.log';
+g_ksSrvGlueDebugLogDst = None;
+## Whether to enable CGI trace back in the server glue.
+g_kfSrvGlueCgiTb = False;
+## Enables glue debug output.
+g_kfSrvGlueDebug = False;
+## Timestamp and pid prefix the glue debug output.
+g_kfSrvGlueDebugTS = True;
+## Whether to dumping CGI environment variables.
+g_kfSrvGlueCgiDumpEnv = False;
+## Whether to dumping CGI script arguments.
+g_kfSrvGlueCgiDumpArgs = False;
+## Enables task scheduler debug output to g_ksSrvGlueDebugLogDst.
+g_kfSrvGlueDebugScheduler = False;
+
+## Enables the SQL trace back.
+g_kfWebUiSqlTrace = False;
+## Enables the explain in the SQL trace back.
+g_kfWebUiSqlTraceExplain = False;
+## Whether the postgresql version supports the TIMING option on EXPLAIN (>= 9.2).
+g_kfWebUiSqlTraceExplainTiming = False;
+## Display time spent processing the page.
+g_kfWebUiProcessedIn = True;
+## Enables WebUI debug output.
+g_kfWebUiDebug = False;
+## Enables WebUI SQL debug output print() calls (requires g_kfWebUiDebug).
+g_kfWebUiSqlDebug = False;
+## Enables the debug panel at the bottom of the page.
+g_kfWebUiDebugPanel = True;
+
+## Profile cgi/admin.py.
+g_kfProfileAdmin = False;
+## Profile cgi/index.py.
+g_kfProfileIndex = False;
+
+## When not None,
+g_ksTestBoxDispXpctLog = '/tmp/testmanager-testboxdisp-xcpt.log'
+## @}
+
diff --git a/src/VBox/ValidationKit/testmanager/core/Makefile.kmk b/src/VBox/ValidationKit/testmanager/core/Makefile.kmk
new file mode 100644
index 00000000..74d882cc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/Makefile.kmk
@@ -0,0 +1,46 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+SUB_DEPTH = ../../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+
+VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py)
+
+$(evalcall def_vbox_validationkit_process_python_sources)
+$(evalcall def_vbox_validationkit_process_js_sources)
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/testmanager/core/__init__.py b/src/VBox/ValidationKit/testmanager/core/__init__.py
new file mode 100644
index 00000000..bfcc7f8b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/__init__.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# $Id: __init__.py $
+
+"""
+TestBox Script - Core Logic.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
diff --git a/src/VBox/ValidationKit/testmanager/core/base.py b/src/VBox/ValidationKit/testmanager/core/base.py
new file mode 100755
index 00000000..eac3d921
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/base.py
@@ -0,0 +1,1514 @@
+# -*- coding: utf-8 -*-
+# $Id: base.py $
+# pylint: disable=too-many-lines
+
+"""
+Test Manager Core - Base Class(es).
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import copy;
+import datetime;
+import json;
+import re;
+import socket;
+import sys;
+import uuid;
+import unittest;
+
+# Validation Kit imports.
+from common import utils;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ long = int # pylint: disable=redefined-builtin,invalid-name
+
+
+class TMExceptionBase(Exception):
+ """
+ For exceptions raised by any TestManager component.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TMTooManyRows(TMExceptionBase):
+ """
+ Too many rows in the result.
+ Used by ModelLogicBase decendants.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TMRowNotFound(TMExceptionBase):
+ """
+ Database row not found.
+ Used by ModelLogicBase decendants.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TMRowAlreadyExists(TMExceptionBase):
+ """
+ Database row already exists (typically raised by addEntry).
+ Used by ModelLogicBase decendants.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TMInvalidData(TMExceptionBase):
+ """
+ Data validation failed.
+ Used by ModelLogicBase decendants.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TMRowInUse(TMExceptionBase):
+ """
+ Database row is in use and cannot be deleted.
+ Used by ModelLogicBase decendants.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TMInFligthCollision(TMExceptionBase):
+ """
+ Database update failed because someone else had already made changes to
+ the data there.
+ Used by ModelLogicBase decendants.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class ModelBase(object): # pylint: disable=too-few-public-methods
+ """
+ Something all classes in the logical model inherits from.
+
+ Not sure if 'logical model' is the right term here.
+ Will see if it has any purpose later on...
+ """
+
+ def __init__(self):
+ pass;
+
+
+class ModelDataBase(ModelBase): # pylint: disable=too-few-public-methods
+ """
+ Something all classes in the data classes in the logical model inherits from.
+ """
+
+ ## Child classes can use this to list array attributes which should use
+ # an empty array ([]) instead of None as database NULL value.
+ kasAltArrayNull = [];
+
+ ## validate
+ ## @{
+ ksValidateFor_Add = 'add';
+ ksValidateFor_AddForeignId = 'add-foreign-id';
+ ksValidateFor_Edit = 'edit';
+ ksValidateFor_Other = 'other';
+ ## @}
+
+
+ ## List of internal attributes which should be ignored by
+ ## getDataAttributes and related machinery
+ kasInternalAttributes = [];
+
+ def __init__(self):
+ ModelBase.__init__(self);
+
+
+ #
+ # Standard methods implemented by combining python magic and hungarian prefixes.
+ #
+
+ def getDataAttributes(self):
+ """
+ Returns a list of data attributes.
+ """
+ asRet = [];
+ asAttrs = dir(self);
+ for sAttr in asAttrs:
+ if sAttr[0] == '_' or sAttr[0] == 'k':
+ continue;
+ if sAttr in self.kasInternalAttributes:
+ continue;
+ oValue = getattr(self, sAttr);
+ if callable(oValue):
+ continue;
+ asRet.append(sAttr);
+ return asRet;
+
+ def initFromOther(self, oOther):
+ """
+ Initialize this object with the values from another instance (child
+ class instance is accepted).
+
+ This serves as a kind of copy constructor.
+
+ Returns self. May raise exception if the type of other object differs
+ or is damaged.
+ """
+ for sAttr in self.getDataAttributes():
+ setattr(self, sAttr, getattr(oOther, sAttr));
+ return self;
+
+ @staticmethod
+ def getHungarianPrefix(sName):
+ """
+ Returns the hungarian prefix of the given name.
+ """
+ for i, _ in enumerate(sName):
+ if sName[i] not in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']:
+ assert re.search('^[A-Z][a-zA-Z0-9]*$', sName[i:]) is not None;
+ return sName[:i];
+ return sName;
+
+ def getAttributeParamNullValues(self, sAttr):
+ """
+ Returns a list of parameter NULL values, with the preferred one being
+ the first element.
+
+ Child classes can override this to handle one or more attributes specially.
+ """
+ sPrefix = self.getHungarianPrefix(sAttr);
+ if sPrefix in ['id', 'uid', 'i', 'off', 'pct']:
+ return [-1, '', '-1',];
+ if sPrefix in ['l', 'c',]:
+ return [long(-1), '', '-1',];
+ if sPrefix == 'f':
+ return ['',];
+ if sPrefix in ['enm', 'ip', 's', 'ts', 'uuid']:
+ return ['',];
+ if sPrefix in ['ai', 'aid', 'al', 'as']:
+ return [[], '', None]; ## @todo ??
+ if sPrefix == 'bm':
+ return ['', [],]; ## @todo bitmaps.
+ raise TMExceptionBase('Unable to classify "%s" (prefix %s)' % (sAttr, sPrefix));
+
+ def isAttributeNull(self, sAttr, oValue):
+ """
+ Checks if the specified attribute value indicates NULL.
+ Return True/False.
+
+ Note! This isn't entirely kosher actually.
+ """
+ if oValue is None:
+ return True;
+ aoNilValues = self.getAttributeParamNullValues(sAttr);
+ return oValue in aoNilValues;
+
+ def _convertAttributeFromParamNull(self, sAttr, oValue):
+ """
+ Converts an attribute from parameter NULL to database NULL value.
+ Returns the new attribute value.
+ """
+ aoNullValues = self.getAttributeParamNullValues(sAttr);
+ if oValue in aoNullValues:
+ oValue = None if sAttr not in self.kasAltArrayNull else [];
+ #
+ # Perform deep conversion on ModelDataBase object and lists of them.
+ #
+ elif isinstance(oValue, list) and oValue and isinstance(oValue[0], ModelDataBase):
+ oValue = copy.copy(oValue);
+ for i, _ in enumerate(oValue):
+ assert isinstance(oValue[i], ModelDataBase);
+ oValue[i] = copy.copy(oValue[i]);
+ oValue[i].convertFromParamNull();
+
+ elif isinstance(oValue, ModelDataBase):
+ oValue = copy.copy(oValue);
+ oValue.convertFromParamNull();
+
+ return oValue;
+
+ def convertFromParamNull(self):
+ """
+ Converts from parameter NULL values to database NULL values (None).
+ Returns self.
+ """
+ for sAttr in self.getDataAttributes():
+ oValue = getattr(self, sAttr);
+ oNewValue = self._convertAttributeFromParamNull(sAttr, oValue);
+ if oValue != oNewValue:
+ setattr(self, sAttr, oNewValue);
+ return self;
+
+ def _convertAttributeToParamNull(self, sAttr, oValue):
+ """
+ Converts an attribute from database NULL to a sepcial value we can pass
+ thru parameter list.
+ Returns the new attribute value.
+ """
+ if oValue is None:
+ oValue = self.getAttributeParamNullValues(sAttr)[0];
+ #
+ # Perform deep conversion on ModelDataBase object and lists of them.
+ #
+ elif isinstance(oValue, list) and oValue and isinstance(oValue[0], ModelDataBase):
+ oValue = copy.copy(oValue);
+ for i, _ in enumerate(oValue):
+ assert isinstance(oValue[i], ModelDataBase);
+ oValue[i] = copy.copy(oValue[i]);
+ oValue[i].convertToParamNull();
+
+ elif isinstance(oValue, ModelDataBase):
+ oValue = copy.copy(oValue);
+ oValue.convertToParamNull();
+
+ return oValue;
+
+ def convertToParamNull(self):
+ """
+ Converts from database NULL values (None) to special values we can
+ pass thru parameters list.
+ Returns self.
+ """
+ for sAttr in self.getDataAttributes():
+ oValue = getattr(self, sAttr);
+ oNewValue = self._convertAttributeToParamNull(sAttr, oValue);
+ if oValue != oNewValue:
+ setattr(self, sAttr, oNewValue);
+ return self;
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ """
+ Validates and convert one attribute.
+ Returns the converted value.
+
+ Child classes can override this to handle one or more attributes specially.
+ Note! oDb can be None.
+ """
+ sPrefix = self.getHungarianPrefix(sAttr);
+
+ if sPrefix in ['id', 'uid']:
+ (oNewValue, sError) = self.validateInt( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
+ elif sPrefix in ['i', 'off', 'pct']:
+ (oNewValue, sError) = self.validateInt( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ iMin = getattr(self, 'kiMin_' + sAttr, 0),
+ iMax = getattr(self, 'kiMax_' + sAttr, 0x7ffffffe));
+ elif sPrefix in ['l', 'c']:
+ (oNewValue, sError) = self.validateLong(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ lMin = getattr(self, 'klMin_' + sAttr, 0),
+ lMax = getattr(self, 'klMax_' + sAttr, None));
+ elif sPrefix == 'f':
+ if not oValue and not fAllowNull: oValue = '0'; # HACK ALERT! Checkboxes are only added when checked.
+ (oNewValue, sError) = self.validateBool(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
+ elif sPrefix == 'ts':
+ (oNewValue, sError) = self.validateTs( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
+ elif sPrefix == 'ip':
+ (oNewValue, sError) = self.validateIp( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
+ elif sPrefix == 'uuid':
+ (oNewValue, sError) = self.validateUuid(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
+ elif sPrefix == 'enm':
+ (oNewValue, sError) = self.validateWord(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ asValid = getattr(self, 'kasValidValues_' + sAttr)); # The list is required.
+ elif sPrefix == 's':
+ (oNewValue, sError) = self.validateStr( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ cchMin = getattr(self, 'kcchMin_' + sAttr, 0),
+ cchMax = getattr(self, 'kcchMax_' + sAttr, 4096),
+ fAllowUnicodeSymbols = getattr(self, 'kfAllowUnicode_' + sAttr, False) );
+ ## @todo al.
+ elif sPrefix == 'aid':
+ (oNewValue, sError) = self.validateListOfInts(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ iMin = 1, iMax = 0x7ffffffe);
+ elif sPrefix == 'as':
+ (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ asValidValues = getattr(self, 'kasValidValues_' + sAttr, None),
+ cchMin = getattr(self, 'kcchMin_' + sAttr, 0 if fAllowNull else 1),
+ cchMax = getattr(self, 'kcchMax_' + sAttr, 4096));
+
+ elif sPrefix == 'bm':
+ ## @todo figure out bitfields.
+ (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
+ else:
+ raise TMExceptionBase('Unable to classify "%s" (prefix %s)' % (sAttr, sPrefix));
+
+ _ = sParam; _ = oDb;
+ return (oNewValue, sError);
+
+ def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ksValidateFor_Other):
+ """
+ Worker for implementing validateAndConvert().
+ """
+ dErrors = {};
+ for sAttr in self.getDataAttributes():
+ oValue = getattr(self, sAttr);
+ sParam = getattr(self, 'ksParam_' + sAttr);
+ aoNilValues = self.getAttributeParamNullValues(sAttr);
+ aoNilValues.append(None);
+
+ (oNewValue, sError) = self._validateAndConvertAttribute(sAttr, sParam, oValue, aoNilValues,
+ sAttr in asAllowNullAttributes, oDb);
+ if oValue != oNewValue:
+ setattr(self, sAttr, oNewValue);
+ if sError is not None:
+ dErrors[sParam] = sError;
+
+ # Check the NULL requirements of the primary ID(s) for the 'add' and 'edit' actions.
+ if enmValidateFor in (ModelDataBase.ksValidateFor_Add,
+ ModelDataBase.ksValidateFor_AddForeignId,
+ ModelDataBase.ksValidateFor_Edit,):
+ fMustBeNull = enmValidateFor == ModelDataBase.ksValidateFor_Add;
+ sAttr = getattr(self, 'ksIdAttr', None);
+ if sAttr is not None:
+ oValue = getattr(self, sAttr);
+ if self.isAttributeNull(sAttr, oValue) != fMustBeNull:
+ sParam = getattr(self, 'ksParam_' + sAttr);
+ sErrMsg = 'Must be NULL!' if fMustBeNull else 'Must not be NULL!'
+ if sParam in dErrors:
+ dErrors[sParam] += ' ' + sErrMsg;
+ else:
+ dErrors[sParam] = sErrMsg;
+
+ return dErrors;
+
+ def validateAndConvert(self, oDb, enmValidateFor = ksValidateFor_Other):
+ """
+ Validates the input and converts valid fields to their right type.
+ Returns a dictionary with per field reports, only invalid fields will
+ be returned, so an empty dictionary means that the data is valid.
+
+ The dictionary keys are ksParam_*.
+
+ Child classes can override _validateAndConvertAttribute to handle
+ selected fields specially. There are also a few class variables that
+ can be used to advice the validation: kcchMin_sAttr, kcchMax_sAttr,
+ kiMin_iAttr, kiMax_iAttr, klMin_lAttr, klMax_lAttr,
+ kasValidValues_enmAttr, and kasAllowNullAttributes.
+ """
+ return self._validateAndConvertWorker(getattr(self, 'kasAllowNullAttributes', []), oDb,
+ enmValidateFor = enmValidateFor);
+
+ def validateAndConvertEx(self, asAllowNullAttributes, oDb, enmValidateFor = ksValidateFor_Other):
+ """
+ Same as validateAndConvert but with custom allow-null list.
+ """
+ return self._validateAndConvertWorker(asAllowNullAttributes, oDb, enmValidateFor = enmValidateFor);
+
+ def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
+ """
+ Calculate the attribute value when initialized from a parameter.
+
+ Returns the new value, with parameter NULL values. Raises exception on
+ invalid parameter value.
+
+ Child classes can override to do special parameter conversion jobs.
+ """
+ sPrefix = self.getHungarianPrefix(sAttr);
+ asValidValues = getattr(self, 'kasValidValues_' + sAttr, None);
+ fAllowNull = sAttr in getattr(self, 'kasAllowNullAttributes', []);
+ if fStrict:
+ if sPrefix == 'f':
+ # HACK ALERT! Checkboxes are only present when checked, so we always have to provide a default.
+ oNewValue = oDisp.getStringParam(sParam, asValidValues, '0');
+ elif sPrefix[0] == 'a':
+ # HACK ALERT! Lists are not present if empty.
+ oNewValue = oDisp.getListOfStrParams(sParam, []);
+ else:
+ oNewValue = oDisp.getStringParam(sParam, asValidValues, None, fAllowNull = fAllowNull);
+ else:
+ if sPrefix[0] == 'a':
+ oNewValue = oDisp.getListOfStrParams(sParam, []);
+ else:
+ assert oValue is not None, 'sAttr=%s' % (sAttr,);
+ oNewValue = oDisp.getStringParam(sParam, asValidValues, oValue, fAllowNull = fAllowNull);
+ return oNewValue;
+
+ def initFromParams(self, oDisp, fStrict = True):
+ """
+ Initialize the object from parameters.
+ The input is not validated at all, except that all parameters must be
+ present when fStrict is True.
+
+ Returns self. Raises exception on invalid parameter value.
+
+ Note! The returned object has parameter NULL values, not database ones!
+ """
+
+ self.convertToParamNull()
+ for sAttr in self.getDataAttributes():
+ oValue = getattr(self, sAttr);
+ oNewValue = self.convertParamToAttribute(sAttr, getattr(self, 'ksParam_' + sAttr), oValue, oDisp, fStrict);
+ if oNewValue != oValue:
+ setattr(self, sAttr, oNewValue);
+ return self;
+
+ def areAttributeValuesEqual(self, sAttr, sPrefix, oValue1, oValue2):
+ """
+ Called to compare two attribute values and python thinks differs.
+
+ Returns True/False.
+
+ Child classes can override this to do special compares of things like arrays.
+ """
+ # Just in case someone uses it directly.
+ if oValue1 == oValue2:
+ return True;
+
+ #
+ # Timestamps can be both string (param) and object (db)
+ # depending on the data source. Compare string values to make
+ # sure we're doing the right thing here.
+ #
+ if sPrefix == 'ts':
+ return str(oValue1) == str(oValue2);
+
+ #
+ # Some generic code handling ModelDataBase children.
+ #
+ if isinstance(oValue1, list) and isinstance(oValue2, list):
+ if len(oValue1) == len(oValue2):
+ for i, _ in enumerate(oValue1):
+ if not isinstance(oValue1[i], ModelDataBase) \
+ or type(oValue1) is not type(oValue2):
+ return False;
+ if not oValue1[i].isEqual(oValue2[i]):
+ return False;
+ return True;
+
+ elif isinstance(oValue1, ModelDataBase) \
+ and type(oValue1) is type(oValue2):
+ return oValue1[i].isEqual(oValue2[i]);
+
+ _ = sAttr;
+ return False;
+
+ def isEqual(self, oOther):
+ """ Compares two instances. """
+ for sAttr in self.getDataAttributes():
+ if getattr(self, sAttr) != getattr(oOther, sAttr):
+ # Delegate the final decision to an overridable method.
+ if not self.areAttributeValuesEqual(sAttr, self.getHungarianPrefix(sAttr),
+ getattr(self, sAttr), getattr(oOther, sAttr)):
+ return False;
+ return True;
+
+ def isEqualEx(self, oOther, asExcludeAttrs):
+ """ Compares two instances, omitting the given attributes. """
+ for sAttr in self.getDataAttributes():
+ if sAttr not in asExcludeAttrs \
+ and getattr(self, sAttr) != getattr(oOther, sAttr):
+ # Delegate the final decision to an overridable method.
+ if not self.areAttributeValuesEqual(sAttr, self.getHungarianPrefix(sAttr),
+ getattr(self, sAttr), getattr(oOther, sAttr)):
+ return False;
+ return True;
+
+ def reinitToNull(self):
+ """
+ Reinitializes the object to (database) NULL values.
+ Returns self.
+ """
+ for sAttr in self.getDataAttributes():
+ setattr(self, sAttr, None);
+ return self;
+
+ def toString(self):
+ """
+ Stringifies the object.
+ Returns string representation.
+ """
+
+ sMembers = '';
+ for sAttr in self.getDataAttributes():
+ oValue = getattr(self, sAttr);
+ sMembers += ', %s=%s' % (sAttr, oValue);
+
+ oClass = type(self);
+ if sMembers == '':
+ return '<%s>' % (oClass.__name__);
+ return '<%s: %s>' % (oClass.__name__, sMembers[2:]);
+
+ def __str__(self):
+ return self.toString();
+
+
+
+ #
+ # New validation helpers.
+ #
+ # These all return (oValue, sError), where sError is None when the value
+ # is valid and an error message when not. On success and in case of
+ # range errors, oValue is converted into the requested type.
+ #
+
+ @staticmethod
+ def validateInt(sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, '']), fAllowNull = True):
+ """ Validates an integer field. """
+ if sValue in aoNilValues:
+ if fAllowNull:
+ return (None if sValue is None else aoNilValues[0], None);
+ return (sValue, 'Mandatory.');
+
+ try:
+ if utils.isString(sValue):
+ iValue = int(sValue, 0);
+ else:
+ iValue = int(sValue);
+ except:
+ return (sValue, 'Not an integer');
+
+ if iValue in aoNilValues:
+ return (aoNilValues[0], None if fAllowNull else 'Mandatory.');
+
+ if iValue < iMin:
+ return (iValue, 'Value too small (min %d)' % (iMin,));
+ if iValue > iMax:
+ return (iValue, 'Value too high (max %d)' % (iMax,));
+ return (iValue, None);
+
+ @staticmethod
+ def validateLong(sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, '']), fAllowNull = True):
+ """ Validates an long integer field. """
+ if sValue in aoNilValues:
+ if fAllowNull:
+ return (None if sValue is None else aoNilValues[0], None);
+ return (sValue, 'Mandatory.');
+ try:
+ if utils.isString(sValue):
+ lValue = long(sValue, 0);
+ else:
+ lValue = long(sValue);
+ except:
+ return (sValue, 'Not a long integer');
+
+ if lValue in aoNilValues:
+ return (aoNilValues[0], None if fAllowNull else 'Mandatory.');
+
+ if lMin is not None and lValue < lMin:
+ return (lValue, 'Value too small (min %d)' % (lMin,));
+ if lMax is not None and lValue > lMax:
+ return (lValue, 'Value too high (max %d)' % (lMax,));
+ return (lValue, None);
+
+ kdTimestampRegex = {
+ len('2012-10-08 01:54:06'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d)$',
+ len('2012-10-08 01:54:06.00'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{2}$',
+ len('2012-10-08 01:54:06.000'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{3}$',
+ len('999999-12-31 00:00:00.00'): r'(\d{6})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{2}$',
+ len('9999-12-31 23:59:59.999999'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{6}$',
+ len('9999-12-31T23:59:59.999999999'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{9}$',
+ };
+
+ @staticmethod
+ def validateTs(sValue, aoNilValues = tuple([None, '']), fAllowNull = True, fRelative = False):
+ """ Validates a timestamp field. """
+ if sValue in aoNilValues:
+ return (sValue, None if fAllowNull else 'Mandatory.');
+ if not utils.isString(sValue):
+ return (sValue, None);
+
+ # Validate and strip off the timezone stuff.
+ if sValue[-1] in 'Zz':
+ sStripped = sValue[:-1];
+ sValue = sStripped + 'Z';
+ elif len(sValue) >= 19 + 3:
+ oRes = re.match(r'^.*[+-](\d\d):(\d\d)$', sValue);
+ if oRes is not None:
+ if int(oRes.group(1)) > 12 or int(oRes.group(2)) >= 60:
+ return (sValue, 'Invalid timezone offset.');
+ sStripped = sValue[:-6];
+ else:
+ sStripped = sValue;
+ else:
+ sStripped = sValue;
+
+ # Used the stripped value length to find regular expression for validating and parsing the timestamp.
+ sError = None;
+ sRegExp = ModelDataBase.kdTimestampRegex.get(len(sStripped), None);
+ if sRegExp:
+ oRes = re.match(sRegExp, sStripped);
+ if oRes is not None:
+ iYear = int(oRes.group(1));
+ if iYear % 4 == 0 and (iYear % 100 != 0 or iYear % 400 == 0):
+ acDaysOfMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+ else:
+ acDaysOfMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+ iMonth = int(oRes.group(2));
+ iDay = int(oRes.group(3));
+ iHour = int(oRes.group(4));
+ iSec = int(oRes.group(5));
+ if iMonth > 12 or (iMonth <= 0 and not fRelative):
+ sError = 'Invalid timestamp month.';
+ elif iDay > acDaysOfMonth[iMonth - 1]:
+ sError = 'Invalid timestamp day-of-month (%02d has %d days).' % (iMonth, acDaysOfMonth[iMonth - 1]);
+ elif iHour > 23:
+ sError = 'Invalid timestamp hour.'
+ elif iSec >= 61:
+ sError = 'Invalid timestamp second.'
+ elif iSec >= 60:
+ sError = 'Invalid timestamp: no leap seconds, please.'
+ else:
+ sError = 'Invalid timestamp (validation regexp: %s).' % (sRegExp,);
+ else:
+ sError = 'Invalid timestamp length.';
+ return (sValue, sError);
+
+ @staticmethod
+ def validateIp(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
+ """ Validates an IP address field. """
+ if sValue in aoNilValues:
+ return (sValue, None if fAllowNull else 'Mandatory.');
+
+ if sValue == '::1':
+ return (sValue, None);
+
+ try:
+ socket.inet_pton(socket.AF_INET, sValue); # pylint: disable=no-member
+ except:
+ try:
+ socket.inet_pton(socket.AF_INET6, sValue); # pylint: disable=no-member
+ except:
+ return (sValue, 'Not a valid IP address.');
+
+ return (sValue, None);
+
+ @staticmethod
+ def validateBool(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
+ """ Validates a boolean field. """
+ if sValue in aoNilValues:
+ return (sValue, None if fAllowNull else 'Mandatory.');
+
+ if sValue in ('True', 'true', '1', True):
+ return (True, None);
+ if sValue in ('False', 'false', '0', False):
+ return (False, None);
+ return (sValue, 'Invalid boolean value.');
+
+ @staticmethod
+ def validateUuid(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
+ """ Validates an UUID field. """
+ if sValue in aoNilValues:
+ return (sValue, None if fAllowNull else 'Mandatory.');
+
+ try:
+ sValue = str(uuid.UUID(sValue));
+ except:
+ return (sValue, 'Invalid UUID value.');
+ return (sValue, None);
+
+ @staticmethod
+ def validateWord(sValue, cchMin = 1, cchMax = 64, asValid = None, aoNilValues = tuple([None, '']), fAllowNull = True):
+ """ Validates a word field. """
+ if sValue in aoNilValues:
+ return (sValue, None if fAllowNull else 'Mandatory.');
+
+ if re.search('[^a-zA-Z0-9_-]', sValue) is not None:
+ sError = 'Single word ([a-zA-Z0-9_-]), please.';
+ elif cchMin is not None and len(sValue) < cchMin:
+ sError = 'Too short, min %s chars' % (cchMin,);
+ elif cchMax is not None and len(sValue) > cchMax:
+ sError = 'Too long, max %s chars' % (cchMax,);
+ elif asValid is not None and sValue not in asValid:
+ sError = 'Invalid value "%s", must be one of: %s' % (sValue, asValid);
+ else:
+ sError = None;
+ return (sValue, sError);
+
+ @staticmethod
+ def validateStr(sValue, cchMin = 0, cchMax = 4096, aoNilValues = tuple([None, '']), fAllowNull = True,
+ fAllowUnicodeSymbols = False):
+ """ Validates a string field. """
+ if sValue in aoNilValues:
+ return (sValue, None if fAllowNull else 'Mandatory.');
+
+ if cchMin is not None and len(sValue) < cchMin:
+ sError = 'Too short, min %s chars' % (cchMin,);
+ elif cchMax is not None and len(sValue) > cchMax:
+ sError = 'Too long, max %s chars' % (cchMax,);
+ elif fAllowUnicodeSymbols is False and utils.hasNonAsciiCharacters(sValue):
+ sError = 'Non-ascii characters not allowed'
+ else:
+ sError = None;
+ return (sValue, sError);
+
+ @staticmethod
+ def validateEmail(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
+ """ Validates a email field."""
+ if sValue in aoNilValues:
+ return (sValue, None if fAllowNull else 'Mandatory.');
+
+ if re.match(r'.+@.+\..+', sValue) is None:
+ return (sValue,'Invalid e-mail format.');
+ return (sValue, None);
+
+ @staticmethod
+ def validateListOfSomething(asValues, aoNilValues = tuple([[], None]), fAllowNull = True):
+ """ Validate a list of some uniform values. Returns a copy of the list (if list it is). """
+ if asValues in aoNilValues or (not asValues and not fAllowNull):
+ return (asValues, None if fAllowNull else 'Mandatory.')
+
+ if not isinstance(asValues, list):
+ return (asValues, 'Invalid data type (%s).' % (type(asValues),));
+
+ asValues = list(asValues); # copy the list.
+ if asValues:
+ oType = type(asValues[0]);
+ for i in range(1, len(asValues)):
+ if type(asValues[i]) is not oType: # pylint: disable=unidiomatic-typecheck
+ return (asValues, 'Invalid entry data type ([0]=%s vs [%d]=%s).' % (oType, i, type(asValues[i])) );
+
+ return (asValues, None);
+
+ @staticmethod
+ def validateListOfStr(asValues, cchMin = None, cchMax = None, asValidValues = None,
+ aoNilValues = tuple([[], None]), fAllowNull = True):
+ """ Validates a list of text items."""
+ (asValues, sError) = ModelDataBase.validateListOfSomething(asValues, aoNilValues, fAllowNull);
+
+ if sError is None and asValues not in aoNilValues and asValues:
+ if not utils.isString(asValues[0]):
+ return (asValues, 'Invalid item data type.');
+
+ if not fAllowNull and cchMin is None:
+ cchMin = 1;
+
+ for sValue in asValues:
+ if asValidValues is not None and sValue not in asValidValues:
+ sThisErr = 'Invalid value "%s".' % (sValue,);
+ elif cchMin is not None and len(sValue) < cchMin:
+ sThisErr = 'Value "%s" is too short, min length is %u chars.' % (sValue, cchMin);
+ elif cchMax is not None and len(sValue) > cchMax:
+ sThisErr = 'Value "%s" is too long, max length is %u chars.' % (sValue, cchMax);
+ else:
+ continue;
+
+ if sError is None:
+ sError = sThisErr;
+ else:
+ sError += ' ' + sThisErr;
+
+ return (asValues, sError);
+
+ @staticmethod
+ def validateListOfInts(asValues, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([[], None]), fAllowNull = True):
+ """ Validates a list of integer items."""
+ (asValues, sError) = ModelDataBase.validateListOfSomething(asValues, aoNilValues, fAllowNull);
+
+ if sError is None and asValues not in aoNilValues and asValues:
+ for i, _ in enumerate(asValues):
+ sValue = asValues[i];
+
+ sThisErr = '';
+ try:
+ iValue = int(sValue);
+ except:
+ sThisErr = 'Invalid integer value "%s".' % (sValue,);
+ else:
+ asValues[i] = iValue;
+ if iValue < iMin:
+ sThisErr = 'Value %d is too small (min %d)' % (iValue, iMin,);
+ elif iValue > iMax:
+ sThisErr = 'Value %d is too high (max %d)' % (iValue, iMax,);
+ else:
+ continue;
+
+ if sError is None:
+ sError = sThisErr;
+ else:
+ sError += ' ' + sThisErr;
+
+ return (asValues, sError);
+
+
+
+ #
+ # Old validation helpers.
+ #
+
+ @staticmethod
+ def _validateInt(dErrors, sName, sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, ''])):
+ """ Validates an integer field. """
+ (sValue, sError) = ModelDataBase.validateInt(sValue, iMin, iMax, aoNilValues, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateIntNN(dErrors, sName, sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, ''])):
+ """ Validates an integer field, not null. """
+ (sValue, sError) = ModelDataBase.validateInt(sValue, iMin, iMax, aoNilValues, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateLong(dErrors, sName, sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, ''])):
+ """ Validates an long integer field. """
+ (sValue, sError) = ModelDataBase.validateLong(sValue, lMin, lMax, aoNilValues, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateLongNN(dErrors, sName, sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, ''])):
+ """ Validates an long integer field, not null. """
+ (sValue, sError) = ModelDataBase.validateLong(sValue, lMin, lMax, aoNilValues, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateTs(dErrors, sName, sValue):
+ """ Validates a timestamp field. """
+ (sValue, sError) = ModelDataBase.validateTs(sValue, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateTsNN(dErrors, sName, sValue):
+ """ Validates a timestamp field, not null. """
+ (sValue, sError) = ModelDataBase.validateTs(sValue, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateIp(dErrors, sName, sValue):
+ """ Validates an IP address field. """
+ (sValue, sError) = ModelDataBase.validateIp(sValue, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateIpNN(dErrors, sName, sValue):
+ """ Validates an IP address field, not null. """
+ (sValue, sError) = ModelDataBase.validateIp(sValue, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateBool(dErrors, sName, sValue):
+ """ Validates a boolean field. """
+ (sValue, sError) = ModelDataBase.validateBool(sValue, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateBoolNN(dErrors, sName, sValue):
+ """ Validates a boolean field, not null. """
+ (sValue, sError) = ModelDataBase.validateBool(sValue, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateUuid(dErrors, sName, sValue):
+ """ Validates an UUID field. """
+ (sValue, sError) = ModelDataBase.validateUuid(sValue, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateUuidNN(dErrors, sName, sValue):
+ """ Validates an UUID field, not null. """
+ (sValue, sError) = ModelDataBase.validateUuid(sValue, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateWord(dErrors, sName, sValue, cchMin = 1, cchMax = 64, asValid = None):
+ """ Validates a word field. """
+ (sValue, sError) = ModelDataBase.validateWord(sValue, cchMin, cchMax, asValid, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateWordNN(dErrors, sName, sValue, cchMin = 1, cchMax = 64, asValid = None):
+ """ Validates a boolean field, not null. """
+ (sValue, sError) = ModelDataBase.validateWord(sValue, cchMin, cchMax, asValid, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateStr(dErrors, sName, sValue, cchMin = 0, cchMax = 4096):
+ """ Validates a string field. """
+ (sValue, sError) = ModelDataBase.validateStr(sValue, cchMin, cchMax, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateStrNN(dErrors, sName, sValue, cchMin = 0, cchMax = 4096):
+ """ Validates a string field, not null. """
+ (sValue, sError) = ModelDataBase.validateStr(sValue, cchMin, cchMax, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateEmail(dErrors, sName, sValue):
+ """ Validates a email field."""
+ (sValue, sError) = ModelDataBase.validateEmail(sValue, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateEmailNN(dErrors, sName, sValue):
+ """ Validates a email field."""
+ (sValue, sError) = ModelDataBase.validateEmail(sValue, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateListOfStr(dErrors, sName, asValues, asValidValues = None):
+ """ Validates a list of text items."""
+ (sValue, sError) = ModelDataBase.validateListOfStr(asValues, asValidValues = asValidValues, fAllowNull = True);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ @staticmethod
+ def _validateListOfStrNN(dErrors, sName, asValues, asValidValues = None):
+ """ Validates a list of text items, not null and len >= 1."""
+ (sValue, sError) = ModelDataBase.validateListOfStr(asValues, asValidValues = asValidValues, fAllowNull = False);
+ if sError is not None:
+ dErrors[sName] = sError;
+ return sValue;
+
+ #
+ # Various helpers.
+ #
+
+ @staticmethod
+ def formatSimpleNowAndPeriod(oDb, tsNow = None, sPeriodBack = None,
+ sTablePrefix = '', sExpCol = 'tsExpire', sEffCol = 'tsEffective'):
+ """
+ Formats a set of tsNow and sPeriodBack arguments for a standard testmanager
+ table.
+
+ If sPeriodBack is given, the query is effective for the period
+ (tsNow - sPeriodBack) thru (tsNow).
+
+ If tsNow isn't given, it defaults to current time.
+
+ Returns the final portion of a WHERE query (start with AND) and maybe an
+ ORDER BY and LIMIT bit if sPeriodBack is given.
+ """
+ if tsNow is not None:
+ if sPeriodBack is not None:
+ sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > (%s::timestamp - %s::interval)\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY ' + sTablePrefix + sExpCol + ' DESC\n'
+ 'LIMIT 1\n'
+ , ( tsNow, sPeriodBack, tsNow));
+ else:
+ sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > %s\n'
+ ' AND ' + sTablePrefix + sEffCol + ' <= %s\n'
+ , ( tsNow, tsNow, ));
+ else:
+ if sPeriodBack is not None:
+ sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > (CURRENT_TIMESTAMP - %s::interval)\n'
+ ' AND ' + sTablePrefix + sEffCol + ' <= CURRENT_TIMESTAMP\n'
+ 'ORDER BY ' + sTablePrefix + sExpCol + ' DESC\n'
+ 'LIMIT 1\n'
+ , ( sPeriodBack, ));
+ else:
+ sRet = ' AND ' + sTablePrefix + sExpCol + ' = \'infinity\'::timestamp\n';
+ return sRet;
+
+ @staticmethod
+ def formatSimpleNowAndPeriodQuery(oDb, sQuery, aBindArgs, tsNow = None, sPeriodBack = None,
+ sTablePrefix = '', sExpCol = 'tsExpire', sEffCol = 'tsEffective'):
+ """
+ Formats a simple query for a standard testmanager table with optional
+ tsNow and sPeriodBack arguments.
+
+ The sQuery and sBindArgs are passed along to oDb.formatBindArgs to form
+ the first part of the query. Must end with an open WHERE statement as
+ we'll be adding the time part starting with 'AND something...'.
+
+ See formatSimpleNowAndPeriod for tsNow and sPeriodBack description.
+
+ Returns the final portion of a WHERE query (start with AND) and maybe an
+ ORDER BY and LIMIT bit if sPeriodBack is given.
+
+ """
+ return oDb.formatBindArgs(sQuery, aBindArgs) \
+ + ModelDataBase.formatSimpleNowAndPeriod(oDb, tsNow, sPeriodBack, sTablePrefix, sExpCol, sEffCol);
+
+
+ #
+ # JSON
+ #
+
+ @staticmethod
+ def stringToJson(sString):
+ """ Converts a string to a JSON value string. """
+ if not utils.isString(sString):
+ sString = utils.toUnicode(sString);
+ if not utils.isString(sString):
+ sString = str(sString);
+ return json.dumps(sString);
+
+ @staticmethod
+ def dictToJson(dDict, dOptions = None):
+ """ Converts a dictionary to a JSON string. """
+ sJson = u'{ ';
+ for i, oKey in enumerate(dDict):
+ if i > 0:
+ sJson += ', ';
+ sJson += '%s: %s' % (ModelDataBase.stringToJson(oKey),
+ ModelDataBase.genericToJson(dDict[oKey], dOptions));
+ return sJson + ' }';
+
+ @staticmethod
+ def listToJson(aoList, dOptions = None):
+ """ Converts list of something to a JSON string. """
+ sJson = u'[ ';
+ for i, oValue in enumerate(aoList):
+ if i > 0:
+ sJson += u', ';
+ sJson += ModelDataBase.genericToJson(oValue, dOptions);
+ return sJson + u' ]';
+
+ @staticmethod
+ def datetimeToJson(oDateTime):
+ """ Converts a datetime instance to a JSON string. """
+ return '"%s"' % (oDateTime,);
+
+
+ @staticmethod
+ def genericToJson(oValue, dOptions = None):
+ """ Converts a generic object to a JSON string. """
+ if isinstance(oValue, ModelDataBase):
+ return oValue.toJson();
+ if isinstance(oValue, dict):
+ return ModelDataBase.dictToJson(oValue, dOptions);
+ if isinstance(oValue, (list, tuple, set, frozenset)):
+ return ModelDataBase.listToJson(oValue, dOptions);
+ if isinstance(oValue, datetime.datetime):
+ return ModelDataBase.datetimeToJson(oValue)
+ return json.dumps(oValue);
+
+ def attribValueToJson(self, sAttr, oValue, dOptions = None):
+ """
+ Converts the attribute value to JSON.
+ Returns JSON (string).
+ """
+ _ = sAttr;
+ return self.genericToJson(oValue, dOptions);
+
+ def toJson(self, dOptions = None):
+ """
+ Converts the object to JSON.
+ Returns JSON (string).
+ """
+ sJson = u'{ ';
+ for iAttr, sAttr in enumerate(self.getDataAttributes()):
+ oValue = getattr(self, sAttr);
+ if iAttr > 0:
+ sJson += ', ';
+ sJson += u'"%s": ' % (sAttr,);
+ sJson += self.attribValueToJson(sAttr, oValue, dOptions);
+ return sJson + u' }';
+
+
+ #
+ # Sub-classes.
+ #
+
+ class DispWrapper(object):
+ """Proxy object."""
+ def __init__(self, oDisp, sAttrFmt):
+ self.oDisp = oDisp;
+ self.sAttrFmt = sAttrFmt;
+ def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False):
+ """See WuiDispatcherBase.getStringParam."""
+ return self.oDisp.getStringParam(self.sAttrFmt % (sName,), asValidValues, sDefault, fAllowNull = fAllowNull);
+ def getListOfStrParams(self, sName, asDefaults = None):
+ """See WuiDispatcherBase.getListOfStrParams."""
+ return self.oDisp.getListOfStrParams(self.sAttrFmt % (sName,), asDefaults);
+ def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None):
+ """See WuiDispatcherBase.getListOfIntParams."""
+ return self.oDisp.getListOfIntParams(self.sAttrFmt % (sName,), iMin, iMax, aiDefaults);
+
+
+
+
+# pylint: disable=no-member,missing-docstring,too-few-public-methods
+class ModelDataBaseTestCase(unittest.TestCase):
+ """
+ Base testcase for ModelDataBase decendants.
+ Derive from this and override setUp.
+ """
+
+ def setUp(self):
+ """
+ Override this! Don't call super!
+ The subclasses are expected to set aoSamples to an array of instance
+ samples. The first entry must be a default object, the subsequent ones
+ are optional and their contents freely choosen.
+ """
+ self.aoSamples = [ModelDataBase(),];
+
+ def testEquality(self):
+ for oSample in self.aoSamples:
+ self.assertEqual(oSample.isEqual(copy.copy(oSample)), True);
+ self.assertIsNotNone(oSample.isEqual(self.aoSamples[0]));
+
+ def testNullConversion(self):
+ if not self.aoSamples[0].getDataAttributes():
+ return;
+ for oSample in self.aoSamples:
+ oCopy = copy.copy(oSample);
+ self.assertEqual(oCopy.convertToParamNull(), oCopy);
+ self.assertEqual(oCopy.isEqual(oSample), False);
+ self.assertEqual(oCopy.convertFromParamNull(), oCopy);
+ self.assertEqual(oCopy.isEqual(oSample), True, '\ngot : %s\nexpected: %s' % (oCopy, oSample,));
+
+ oCopy = copy.copy(oSample);
+ self.assertEqual(oCopy.convertToParamNull(), oCopy);
+ oCopy2 = copy.copy(oCopy);
+ self.assertEqual(oCopy.convertToParamNull(), oCopy);
+ self.assertEqual(oCopy.isEqual(oCopy2), True);
+ self.assertEqual(oCopy.convertToParamNull(), oCopy);
+ self.assertEqual(oCopy.isEqual(oCopy2), True);
+
+ oCopy = copy.copy(oSample);
+ self.assertEqual(oCopy.convertFromParamNull(), oCopy);
+ oCopy2 = copy.copy(oCopy);
+ self.assertEqual(oCopy.convertFromParamNull(), oCopy);
+ self.assertEqual(oCopy.isEqual(oCopy2), True);
+ self.assertEqual(oCopy.convertFromParamNull(), oCopy);
+ self.assertEqual(oCopy.isEqual(oCopy2), True);
+
+ def testReinitToNull(self):
+ oFirst = copy.copy(self.aoSamples[0]);
+ self.assertEqual(oFirst.reinitToNull(), oFirst);
+ for oSample in self.aoSamples:
+ oCopy = copy.copy(oSample);
+ self.assertEqual(oCopy.reinitToNull(), oCopy);
+ self.assertEqual(oCopy.isEqual(oFirst), True);
+
+ def testValidateAndConvert(self):
+ for oSample in self.aoSamples:
+ oCopy = copy.copy(oSample);
+ oCopy.convertToParamNull();
+ dError1 = oCopy.validateAndConvert(None);
+
+ oCopy2 = copy.copy(oCopy);
+ self.assertEqual(oCopy.validateAndConvert(None), dError1);
+ self.assertEqual(oCopy.isEqual(oCopy2), True);
+
+ def testInitFromParams(self):
+ class DummyDisp(object):
+ def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False):
+ _ = sName; _ = asValidValues; _ = fAllowNull;
+ return sDefault;
+ def getListOfStrParams(self, sName, asDefaults = None):
+ _ = sName;
+ return asDefaults;
+ def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None):
+ _ = sName; _ = iMin; _ = iMax;
+ return aiDefaults;
+
+ for oSample in self.aoSamples:
+ oCopy = copy.copy(oSample);
+ self.assertEqual(oCopy.initFromParams(DummyDisp(), fStrict = False), oCopy);
+
+ def testToString(self):
+ for oSample in self.aoSamples:
+ self.assertIsNotNone(oSample.toString());
+
+
+class FilterCriterionValueAndDescription(object):
+ """
+ A filter criterion value and its description.
+ """
+
+ def __init__(self, oValue, sDesc, cTimes = None, sHover = None, fIrrelevant = False):
+ self.oValue = oValue; ##< Typically the ID of something in the database.
+ self.sDesc = sDesc; ##< What to display.
+ self.cTimes = cTimes; ##< Number of times the value occurs in the result set. None if not given.
+ self.sHover = sHover; ##< Optional hover/title string.
+ self.fIrrelevant = fIrrelevant; ##< Irrelevant filter option, only present because it's selected
+ self.aoSubs = []; ##< References to FilterCriterion.oSub.aoPossible.
+
+
+class FilterCriterion(object):
+ """
+ A filter criterion.
+ """
+
+ ## @name The state.
+ ## @{
+ ksState_NotSelected = 'not-selected';
+ ksState_Selected = 'selected';
+ ## @}
+
+ ## @name The kind of filtering.
+ ## @{
+ ## 'Element of' by default, 'not an element of' when fInverted is False.
+ ksKind_ElementOfOrNot = 'element-of-or-not';
+ ## The criterion is a special one and cannot be inverted.
+ ksKind_Special = 'special';
+ ## @}
+
+ ## @name The value type.
+ ## @{
+ ksType_UInt = 'uint'; ##< unsigned integer value.
+ ksType_UIntNil = 'uint-nil'; ##< unsigned integer value, with nil.
+ ksType_String = 'string'; ##< string value.
+ ksType_Ranges = 'ranges'; ##< List of (unsigned) integer ranges.
+ ## @}
+
+ def __init__(self, sName, sVarNm = None, sType = ksType_UInt, # pylint: disable=too-many-arguments
+ sState = ksState_NotSelected, sKind = ksKind_ElementOfOrNot,
+ sTable = None, sColumn = None, asTables = None, oSub = None):
+ assert len(sVarNm) == 2; # required by wuimain.py for filtering.
+ self.sName = sName;
+ self.sState = sState;
+ self.sType = sType;
+ self.sKind = sKind;
+ self.sVarNm = sVarNm;
+ self.aoSelected = []; ##< User input from sVarNm. Single value, type according to sType.
+ self.sInvVarNm = 'i' + sVarNm if sKind == self.ksKind_ElementOfOrNot else None;
+ self.fInverted = False; ##< User input from sInvVarNm. Inverts the operation (-> not an element of).
+ self.aoPossible = []; ##< type: list[FilterCriterionValueAndDescription]
+ assert (sTable is None and asTables is None) or ((sTable is not None) != (asTables is not None)), \
+ '%s %s' % (sTable, asTables);
+ self.asTables = [sTable,] if sTable is not None else asTables;
+ assert sColumn is None or len(self.asTables) == 1, '%s %s' % (self.asTables, sColumn);
+ self.sColumn = sColumn; ##< Normally only applicable if one table.
+ self.fExpanded = None; ##< Tristate (None, False, True)
+ self.oSub = oSub; ##< type: FilterCriterion
+
+
+class ModelFilterBase(ModelBase):
+ """
+ Base class for filters.
+
+ Filters are used to narrow down data that is displayed in a list or
+ report. This class differs a little from ModelDataBase in that it is not
+ tied to a database table, but one or more database queries that are
+ typically rather complicated.
+
+ The filter object has two roles:
+
+ 1. It is used by a ModelLogicBase descendant to store the available
+ filtering options for data begin displayed.
+
+ 2. It decodes and stores the filtering options submitted by the user so
+ a ModeLogicBase descendant can use it to construct WHERE statements.
+
+ The ModelFilterBase class is related to the ModelDataBase class in that it
+ decodes user parameters and stores data, however it is not a descendant.
+
+ Note! In order to reduce URL lengths, we use very very brief parameter
+ names for the filters.
+ """
+
+ def __init__(self):
+ ModelBase.__init__(self);
+ self.aCriteria = [] # type: list[FilterCriterion]
+
+ def _initFromParamsWorker(self, oDisp, oCriterion): # (,FilterCriterion)
+ """ Worker for initFromParams. """
+ if oCriterion.sType == FilterCriterion.ksType_UInt:
+ oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = 0, aiDefaults = []);
+ elif oCriterion.sType == FilterCriterion.ksType_UIntNil:
+ oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = -1, aiDefaults = []);
+ elif oCriterion.sType == FilterCriterion.ksType_String:
+ oCriterion.aoSelected = oDisp.getListOfStrParams(oCriterion.sVarNm, asDefaults = []);
+ if len(oCriterion.aoSelected) > 100:
+ raise TMExceptionBase('Variable %s has %u value, max allowed is 100!'
+ % (oCriterion.sVarNm, len(oCriterion.aoSelected)));
+ for sValue in oCriterion.aoSelected:
+ if len(sValue) > 64 \
+ or '\'' in sValue \
+ or sValue[-1] == '\\':
+ raise TMExceptionBase('Variable %s has an illegal value "%s"!' % (oCriterion.sVarNm, sValue));
+ elif oCriterion.sType == FilterCriterion.ksType_Ranges:
+ def convertRangeNumber(sValue):
+ """ Helper """
+ sValue = sValue.strip();
+ if sValue and sValue not in ('inf', 'Inf', 'INf', 'INF', 'InF', 'iNf', 'iNF', 'inF',):
+ try: return int(sValue);
+ except: pass;
+ return None;
+
+ for sRange in oDisp.getStringParam(oCriterion.sVarNm, sDefault = '').split(','):
+ sRange = sRange.strip();
+ if sRange and sRange != '-' and any(ch.isdigit() for ch in sRange):
+ asValues = sRange.split('-');
+ if len(asValues) == 1:
+ asValues = [asValues[0], asValues[0]];
+ elif len(asValues) > 2:
+ asValues = [asValues[0], asValues[-1]];
+ tTuple = (convertRangeNumber(asValues[0]), convertRangeNumber(asValues[1]));
+ if tTuple[0] is not None and tTuple[1] is not None and tTuple[0] > tTuple[1]:
+ tTuple = (tTuple[1], tTuple[0]);
+ oCriterion.aoSelected.append(tTuple);
+ else:
+ assert False;
+ if oCriterion.aoSelected:
+ oCriterion.sState = FilterCriterion.ksState_Selected;
+ else:
+ oCriterion.sState = FilterCriterion.ksState_NotSelected;
+
+ if oCriterion.sKind == FilterCriterion.ksKind_ElementOfOrNot:
+ oCriterion.fInverted = oDisp.getBoolParam(oCriterion.sInvVarNm, fDefault = False);
+
+ if oCriterion.oSub is not None:
+ self._initFromParamsWorker(oDisp, oCriterion.oSub);
+ return;
+
+ def initFromParams(self, oDisp): # type: (WuiDispatcherBase) -> self
+ """
+ Initialize the object from parameters.
+
+ Returns self. Raises exception on invalid parameter value.
+ """
+
+ for oCriterion in self.aCriteria:
+ self._initFromParamsWorker(oDisp, oCriterion);
+ return self;
+
+ def strainParameters(self, dParams, aAdditionalParams = None):
+ """ Filters just the parameters relevant to this filter, returning a copy. """
+
+ # Collect the parameter names.
+ dWanted = {};
+ for oCrit in self.aCriteria:
+ dWanted[oCrit.sVarNm] = 1;
+ if oCrit.sInvVarNm:
+ dWanted[oCrit.sInvVarNm] = 1;
+
+ # Add additional stuff.
+ if aAdditionalParams:
+ for sParam in aAdditionalParams:
+ dWanted[sParam] = 1;
+
+ # To the straining.
+ dRet = {};
+ for sKey in dParams:
+ if sKey in dWanted:
+ dRet[sKey] = dParams[sKey];
+ return dRet;
+
+
+class ModelLogicBase(ModelBase): # pylint: disable=too-few-public-methods
+ """
+ Something all classes in the logic classes the logical model inherits from.
+ """
+
+ def __init__(self, oDb):
+ ModelBase.__init__(self);
+
+ #
+ # Note! Do not create a connection here if None, we need to DB share
+ # connection with all other logic objects so we can perform half
+ # complex transactions involving several logic objects.
+ #
+ self._oDb = oDb;
+
+ def getDbConnection(self):
+ """
+ Gets the database connection.
+ This should only be used for instantiating other ModelLogicBase children.
+ """
+ return self._oDb;
+
+ def _dbRowsToModelDataList(self, oModelDataType, aaoRows = None):
+ """
+ Helper for conerting a simple fetch into a list of ModelDataType python objects.
+
+ If aaoRows is None, we'll fetchAll from the database ourselves.
+
+ The oModelDataType must be a class derived from ModelDataBase and implement
+ the initFormDbRow method.
+
+ Returns a list of oModelDataType instances.
+ """
+ assert issubclass(oModelDataType, ModelDataBase);
+ aoRet = [];
+ if aaoRows is None:
+ aaoRows = self._oDb.fetchAll();
+ for aoRow in aaoRows:
+ aoRet.append(oModelDataType().initFromDbRow(aoRow));
+ return aoRet;
+
+
+
+class AttributeChangeEntry(object): # pylint: disable=too-few-public-methods
+ """
+ Data class representing the changes made to one attribute.
+ """
+
+ def __init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText):
+ self.sAttr = sAttr;
+ self.oNewRaw = oNewRaw;
+ self.oOldRaw = oOldRaw;
+ self.sNewText = sNewText;
+ self.sOldText = sOldText;
+
+class AttributeChangeEntryPre(AttributeChangeEntry): # pylint: disable=too-few-public-methods
+ """
+ AttributeChangeEntry for preformatted values.
+ """
+
+ def __init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText):
+ AttributeChangeEntry.__init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText);
+
+class ChangeLogEntry(object): # pylint: disable=too-few-public-methods
+ """
+ A change log entry returned by the fetchChangeLog method typically
+ implemented by ModelLogicBase child classes.
+ """
+
+ def __init__(self, uidAuthor, sAuthor, tsEffective, tsExpire, oNewRaw, oOldRaw, aoChanges):
+ self.uidAuthor = uidAuthor;
+ self.sAuthor = sAuthor;
+ self.tsEffective = tsEffective;
+ self.tsExpire = tsExpire;
+ self.oNewRaw = oNewRaw;
+ self.oOldRaw = oOldRaw; # Note! NULL for the last entry.
+ self.aoChanges = aoChanges;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/build.py b/src/VBox/ValidationKit/testmanager/core/build.py
new file mode 100755
index 00000000..bfe50ca5
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/build.py
@@ -0,0 +1,891 @@
+# -*- coding: utf-8 -*-
+# $Id: build.py $
+
+"""
+Test Manager - Builds.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os;
+import unittest;
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core import coreconsts;
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \
+ TMTooManyRows, TMInvalidData, TMRowNotFound, TMRowInUse;
+
+
+class BuildCategoryData(ModelDataBase):
+ """
+ A build category.
+ """
+
+ ksIdAttr = 'idBuildCategory';
+
+ ksParam_idBuildCategory = 'BuildCategory_idBuildCategory';
+ ksParam_sProduct = 'BuildCategory_sProduct';
+ ksParam_sRepository = 'BuildCategory_sRepository';
+ ksParam_sBranch = 'BuildCategory_sBranch';
+ ksParam_sType = 'BuildCategory_sType';
+ ksParam_asOsArches = 'BuildCategory_asOsArches';
+
+ kasAllowNullAttributes = ['idBuildCategory', ];
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idBuildCategory = None;
+ self.sProduct = None;
+ self.sRepository = None;
+ self.sBranch = None;
+ self.sType = None;
+ self.asOsArches = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the object from a SELECT * FROM BuildCategories row.
+ Returns self. Raises exception if aoRow is None.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('BuildCategory not found.');
+
+ self.idBuildCategory = aoRow[0];
+ self.sProduct = aoRow[1];
+ self.sRepository = aoRow[2];
+ self.sBranch = aoRow[3];
+ self.sType = aoRow[4];
+ self.asOsArches = sorted(aoRow[5]);
+ return self;
+
+ def initFromDbWithId(self, oDb, idBuildCategory, tsNow = None, sPeriodBack = None):
+ """
+ Initialize from the database, given the ID of a row.
+ """
+ _ = tsNow; _ = sPeriodBack; # No history in this table.
+ oDb.execute('SELECT * FROM BuildCategories WHERE idBuildCategory = %s', (idBuildCategory,));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idBuildCategory=%s not found' % (idBuildCategory, ));
+ return self.initFromDbRow(aoRow);
+
+ def initFromValues(self, sProduct, sRepository, sBranch, sType, asOsArches, idBuildCategory = None):
+ """
+ Reinitializes form a set of values.
+ return self.
+ """
+ self.idBuildCategory = idBuildCategory;
+ self.sProduct = sProduct;
+ self.sRepository = sRepository;
+ self.sBranch = sBranch;
+ self.sType = sType;
+ self.asOsArches = asOsArches;
+ return self;
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ # Handle sType and asOsArches specially.
+ if sAttr == 'sType':
+ (oNewValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue,
+ aoNilValues, fAllowNull, oDb);
+ if sError is None and self.sType.lower() != self.sType:
+ sError = 'Invalid build type value';
+
+ elif sAttr == 'asOsArches':
+ (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ asValidValues = coreconsts.g_kasOsDotCpusAll);
+ if sError is not None and oNewValue is not None:
+ oNewValue = sorted(oNewValue); # Must be sorted!
+
+ else:
+ return ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+
+ return (oNewValue, sError);
+
+ def matchesOsArch(self, sOs, sArch):
+ """ Checks if the build matches the given OS and architecture. """
+ if sOs + '.' + sArch in self.asOsArches:
+ return True;
+ if sOs + '.noarch' in self.asOsArches:
+ return True;
+ if 'os-agnostic.' + sArch in self.asOsArches:
+ return True;
+ if 'os-agnostic.noarch' in self.asOsArches:
+ return True;
+ return False;
+
+
+class BuildCategoryLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Build categories database logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches testboxes for listing.
+
+ Returns an array (list) of UserAccountData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = tsNow; _ = aiSortColumns;
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildCategories\n'
+ 'ORDER BY sProduct, sRepository, sBranch, sType, idBuildCategory\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+
+ aoRows = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRows.append(BuildCategoryData().initFromDbRow(self._oDb.fetchOne()));
+ return aoRows;
+
+ def fetchForCombo(self):
+ """
+ Gets the list of Build Categories for a combo box.
+ Returns an array of (value [idBuildCategory], drop-down-name [info],
+ hover-text [info]) tuples.
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildCategories\n'
+ 'ORDER BY sProduct, sBranch, sType, asOsArches')
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ oData = BuildCategoryData().initFromDbRow(aoRow)
+
+ sInfo = '%s / %s / %s / %s' % \
+ (oData.sProduct,
+ oData.sBranch,
+ oData.sType,
+ ', '.join(oData.asOsArches))
+
+ # Make short info string if necessary
+ sInfo = sInfo if len(sInfo) < 70 else (sInfo[:70] + '...')
+
+ oInfoItem = (oData.idBuildCategory, sInfo, sInfo)
+ aoRet.append(oInfoItem)
+
+ return aoRet
+
+ def addEntry(self, oData, uidAuthor = None, fCommit = False):
+ """
+ Standard method for adding a build category.
+ """
+
+ # Lazy bird warning! Reuse the soft addBuildCategory method.
+ self.addBuildCategory(oData, fCommit);
+ _ = uidAuthor;
+ return True;
+
+ def removeEntry(self, uidAuthor, idBuildCategory, fCascade = False, fCommit = False):
+ """
+ Tries to delete the build category.
+ Note! Does not implement cascading. This is intentional!
+ """
+
+ #
+ # Check that the build category isn't used by anyone.
+ #
+ self._oDb.execute('SELECT COUNT(idBuild)\n'
+ 'FROM Builds\n'
+ 'WHERE idBuildCategory = %s\n'
+ , (idBuildCategory,));
+ cBuilds = self._oDb.fetchOne()[0];
+ if cBuilds > 0:
+ raise TMRowInUse('Build category #%d is used by %d builds and can therefore not be deleted.'
+ % (idBuildCategory, cBuilds,));
+
+ #
+ # Ok, it's not used, so just delete it.
+ # (No history on this table. This code is for typos.)
+ #
+ self._oDb.execute('DELETE FROM Builds\n'
+ 'WHERE idBuildCategory = %s\n'
+ , (idBuildCategory,));
+
+ self._oDb.maybeCommit(fCommit);
+ _ = uidAuthor; _ = fCascade;
+ return True;
+
+ def cachedLookup(self, idBuildCategory):
+ """
+ Looks up the most recent BuildCategoryData object for idBuildCategory
+ via an object cache.
+
+ Returns a shared BuildCategoryData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('BuildCategoryData');
+ oEntry = self.dCache.get(idBuildCategory, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildCategories\n'
+ 'WHERE idBuildCategory = %s\n'
+ , (idBuildCategory, ));
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ oEntry = BuildCategoryData();
+ oEntry.initFromDbRow(aaoRow);
+ self.dCache[idBuildCategory] = oEntry;
+ return oEntry;
+
+ #
+ # Other methods.
+ #
+
+ def tryFetch(self, idBuildCategory):
+ """
+ Try fetch the build category with the given ID.
+ Returns BuildCategoryData instance if found, None if not found.
+ May raise exception on database error.
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildCategories\n'
+ 'WHERE idBuildCategory = %s\n'
+ , (idBuildCategory,))
+ aaoRows = self._oDb.fetchAll()
+ if not aaoRows:
+ return None;
+ if len(aaoRows) != 1:
+ raise self._oDb.integrityException('Duplicates in BuildCategories: %s' % (aaoRows,));
+ return BuildCategoryData().initFromDbRow(aaoRows[0])
+
+ def tryFindByData(self, oData):
+ """
+ Tries to find the matching build category from the sProduct, sBranch,
+ sType and asOsArches members of oData.
+
+ Returns a valid build category ID and an updated oData object if found.
+ Returns None and unmodified oData object if not found.
+ May raise exception on database error.
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildCategories\n'
+ 'WHERE sProduct = %s\n'
+ ' AND sRepository = %s\n'
+ ' AND sBranch = %s\n'
+ ' AND sType = %s\n'
+ ' AND asOsArches = %s\n'
+ , ( oData.sProduct,
+ oData.sRepository,
+ oData.sBranch,
+ oData.sType,
+ sorted(oData.asOsArches),
+ ));
+ aaoRows = self._oDb.fetchAll();
+ if not aaoRows:
+ return None;
+ if len(aaoRows) > 1:
+ raise self._oDb.integrityException('Duplicates in BuildCategories: %s' % (aaoRows,));
+
+ oData.initFromDbRow(aaoRows[0]);
+ return oData.idBuildCategory;
+
+ def addBuildCategory(self, oData, fCommit = False):
+ """
+ Add Build Category record into the database if needed, returning updated oData.
+ Raises exception on input and database errors.
+ """
+
+ # Check BuildCategoryData before do anything
+ dDataErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dDataErrors:
+ raise TMInvalidData('Invalid data passed to addBuildCategory(): %s' % (dDataErrors,));
+
+ # Does it already exist?
+ if self.tryFindByData(oData) is None:
+ # No, We'll have to add it.
+ self._oDb.execute('INSERT INTO BuildCategories (sProduct, sRepository, sBranch, sType, asOsArches)\n'
+ 'VALUES (%s, %s, %s, %s, %s)\n'
+ 'RETURNING idBuildCategory'
+ , ( oData.sProduct,
+ oData.sRepository,
+ oData.sBranch,
+ oData.sType,
+ sorted(oData.asOsArches),
+ ));
+ oData.idBuildCategory = self._oDb.fetchOne()[0];
+
+ self._oDb.maybeCommit(fCommit);
+ return oData;
+
+
+class BuildData(ModelDataBase):
+ """
+ A build.
+ """
+
+ ksIdAttr = 'idBuild';
+
+ ksParam_idBuild = 'Build_idBuild';
+ ksParam_tsCreated = 'Build_tsCreated';
+ ksParam_tsEffective = 'Build_tsEffective';
+ ksParam_tsExpire = 'Build_tsExpire';
+ ksParam_uidAuthor = 'Build_uidAuthor';
+ ksParam_idBuildCategory = 'Build_idBuildCategory';
+ ksParam_iRevision = 'Build_iRevision';
+ ksParam_sVersion = 'Build_sVersion';
+ ksParam_sLogUrl = 'Build_sLogUrl';
+ ksParam_sBinaries = 'Build_sBinaries';
+ ksParam_fBinariesDeleted = 'Build_fBinariesDeleted';
+
+ kasAllowNullAttributes = ['idBuild', 'tsCreated', 'tsEffective', 'tsExpire', 'uidAuthor', 'tsCreated', 'sLogUrl'];
+
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idBuild = None;
+ self.tsCreated = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.idBuildCategory = None;
+ self.iRevision = None;
+ self.sVersion = None;
+ self.sLogUrl = None;
+ self.sBinaries = None;
+ self.fBinariesDeleted = False;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the object from a SELECT * FROM Builds row.
+ Returns self. Raises exception if aoRow is None.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Build not found.');
+
+ self.idBuild = aoRow[0];
+ self.tsCreated = aoRow[1];
+ self.tsEffective = aoRow[2];
+ self.tsExpire = aoRow[3];
+ self.uidAuthor = aoRow[4];
+ self.idBuildCategory = aoRow[5];
+ self.iRevision = aoRow[6];
+ self.sVersion = aoRow[7];
+ self.sLogUrl = aoRow[8];
+ self.sBinaries = aoRow[9];
+ self.fBinariesDeleted = aoRow[10];
+ return self;
+
+ def initFromDbWithId(self, oDb, idBuild, tsNow = None, sPeriodBack = None):
+ """
+ Initialize from the database, given the ID of a row.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM Builds\n'
+ 'WHERE idBuild = %s\n'
+ , ( idBuild,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idBuild=%s not found (tsNow=%s sPeriodBack=%s)' % (idBuild, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+ def areFilesStillThere(self):
+ """
+ Try check if the build files are still there.
+
+ Returns True if they are, None if we cannot tell, and False if one or
+ more are missing.
+ """
+ if self.fBinariesDeleted:
+ return False;
+
+ for sBinary in self.sBinaries.split(','):
+ sBinary = sBinary.strip();
+ if not sBinary:
+ continue;
+ # Same URL tests as in webutils.downloadFile().
+ if sBinary.startswith('http://') \
+ or sBinary.startswith('https://') \
+ or sBinary.startswith('ftp://'):
+ # URL - don't bother trying to verify that (we don't use it atm).
+ fRc = None;
+ else:
+ # File.
+ if config.g_ksBuildBinRootDir is not None:
+ sFullPath = os.path.join(config.g_ksBuildBinRootDir, sBinary);
+ fRc = os.path.isfile(sFullPath);
+ if not fRc \
+ and not os.path.isfile(os.path.join(config.g_ksBuildBinRootDir, config.g_ksBuildBinRootFile)):
+ fRc = None; # Root file missing, so the share might not be mounted correctly.
+ else:
+ fRc = None;
+ if fRc is not True:
+ return fRc;
+
+ return True;
+
+
+class BuildDataEx(BuildData):
+ """
+ Complete data set.
+ """
+
+ kasInternalAttributes = [ 'oCat', ];
+
+ def __init__(self):
+ BuildData.__init__(self);
+ self.oCat = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT Builds.*, BuildCategories.* FROM Builds, BuildCategories query.
+ Returns self. Raises exception if aoRow is None.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Build not found.');
+ BuildData.initFromDbRow(self, aoRow);
+ self.oCat = BuildCategoryData().initFromDbRow(aoRow[11:]);
+ return self;
+
+ def initFromDbWithId(self, oDb, idBuild, tsNow = None, sPeriodBack = None):
+ """
+ Reinitialize from database given a row ID.
+ Returns self. Raises exception on database error or if the ID is invalid.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT Builds.*, BuildCategories.*\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE idBuild = %s\n'
+ ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ , ( idBuild,), tsNow, sPeriodBack, 'Builds.'));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idBuild=%s not found (tsNow=%s sPeriodBack=%s)' % (idBuild, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+ def convertFromParamNull(self):
+ raise TMExceptionBase('Not implemented');
+
+ def isEqual(self, oOther):
+ raise TMExceptionBase('Not implemented');
+
+
+
+class BuildLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Build database logic (covers build categories as well as builds).
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ #
+ # Standard methods.
+ #
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches builds for listing.
+
+ Returns an array (list) of BuildDataEx items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ ' AND Builds.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY tsCreated DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ ' AND Builds.tsExpire > %s\n'
+ ' AND Builds.tsEffective <= %s\n'
+ 'ORDER BY tsCreated DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRows = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRows.append(BuildDataEx().initFromDbRow(self._oDb.fetchOne()));
+ return aoRows;
+
+ def addEntry(self, oBuildData, uidAuthor = None, fCommit = False):
+ """
+ Adds the build to the database, optionally adding the build category if
+ a BuildDataEx object used and it's necessary.
+
+ Returns updated data object. Raises exception on failure.
+ """
+
+ # Find/Add the build category if specified.
+ if isinstance(oBuildData, BuildDataEx) \
+ and oBuildData.idBuildCategory is None:
+ BuildCategoryLogic(self._oDb).addBuildCategory(oBuildData.oCat, fCommit = False);
+ oBuildData.idBuildCategory = oBuildData.oCat.idBuildCategory;
+
+ # Add the build.
+ self._oDb.execute('INSERT INTO Builds (uidAuthor,\n'
+ ' idBuildCategory,\n'
+ ' iRevision,\n'
+ ' sVersion,\n'
+ ' sLogUrl,\n'
+ ' sBinaries,\n'
+ ' fBinariesDeleted)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s)\n'
+ 'RETURNING idBuild, tsCreated\n'
+ , ( uidAuthor,
+ oBuildData.idBuildCategory,
+ oBuildData.iRevision,
+ oBuildData.sVersion,
+ oBuildData.sLogUrl,
+ oBuildData.sBinaries,
+ oBuildData.fBinariesDeleted,
+ ));
+ aoRow = self._oDb.fetchOne();
+ oBuildData.idBuild = aoRow[0];
+ oBuildData.tsCreated = aoRow[1];
+
+ self._oDb.maybeCommit(fCommit);
+ return oBuildData;
+
+ def editEntry(self, oData, uidAuthor = None, fCommit = False):
+ """Modify database record"""
+
+ #
+ # Validate input and get current data.
+ #
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
+ oOldData = BuildData().initFromDbWithId(self._oDb, oData.idBuild);
+
+ #
+ # Do the work.
+ #
+ if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor' ]):
+ self._historizeBuild(oData.idBuild);
+ self._oDb.execute('INSERT INTO Builds (uidAuthor,\n'
+ ' idBuild,\n'
+ ' tsCreated,\n'
+ ' idBuildCategory,\n'
+ ' iRevision,\n'
+ ' sVersion,\n'
+ ' sLogUrl,\n'
+ ' sBinaries,\n'
+ ' fBinariesDeleted)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)\n'
+ 'RETURNING idBuild, tsCreated\n'
+ , ( uidAuthor,
+ oData.idBuild,
+ oData.tsCreated,
+ oData.idBuildCategory,
+ oData.iRevision,
+ oData.sVersion,
+ oData.sLogUrl,
+ oData.sBinaries,
+ oData.fBinariesDeleted,
+ ));
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def removeEntry(self, uidAuthor, idBuild, fCascade = False, fCommit = False):
+ """
+ Historize record
+ """
+
+ #
+ # No non-historic refs here, so just go ahead and expire the build.
+ #
+ _ = fCascade;
+ _ = uidAuthor; ## @todo record deleter.
+
+ self._historizeBuild(idBuild, None);
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def cachedLookup(self, idBuild):
+ """
+ Looks up the most recent BuildDataEx object for idBuild
+ via an object cache.
+
+ Returns a shared BuildDataEx object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('BuildDataEx');
+ oEntry = self.dCache.get(idBuild, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT Builds.*, BuildCategories.*\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE Builds.idBuild = %s\n'
+ ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idBuild, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT Builds.*, BuildCategories.*\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE Builds.idBuild = %s\n'
+ ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idBuild, ));
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBuild));
+
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ oEntry = BuildDataEx();
+ oEntry.initFromDbRow(aaoRow);
+ self.dCache[idBuild] = oEntry;
+ return oEntry;
+
+
+ #
+ # Other methods.
+ #
+
+ def tryFindSameBuildForOsArch(self, oBuildEx, sOs, sCpuArch):
+ """
+ Attempts to find a matching build for the given OS.ARCH. May return
+ the input build if if matches.
+
+ Returns BuildDataEx instance if found, None if none. May raise
+ exception on database error.
+ """
+
+ if oBuildEx.oCat.matchesOsArch(sOs, sCpuArch):
+ return oBuildEx;
+
+ self._oDb.execute('SELECT Builds.*, BuildCategories.*\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE BuildCategories.sProduct = %s\n'
+ ' AND BuildCategories.sBranch = %s\n'
+ ' AND BuildCategories.sType = %s\n'
+ ' AND ( %s = ANY(BuildCategories.asOsArches)\n'
+ ' OR %s = ANY(BuildCategories.asOsArches)\n'
+ ' OR %s = ANY(BuildCategories.asOsArches))\n'
+ ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ ' AND Builds.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND Builds.iRevision = %s\n'
+ ' AND Builds.sRelease = %s\n'
+ ' AND Builds.fBinariesDeleted IS FALSE\n'
+ 'ORDER BY tsCreated DESC\n'
+ 'LIMIT 4096\n' # stay sane.
+ , (oBuildEx.oCat.sProduct,
+ oBuildEx.oCat.sBranch,
+ oBuildEx.oCat.sType,
+ '%s.%s' % (sOs, sCpuArch),
+ '%s.noarch' % (sOs,),
+ 'os-agnostic.%s' % (sCpuArch,),
+ 'os-agnostic.noarch',
+ oBuildEx.iRevision,
+ oBuildEx.sRelease,
+ ) );
+ aaoRows = self._oDb.fetchAll();
+
+ for aoRow in aaoRows:
+ oBuildExRet = BuildDataEx().initFromDbRow(aoRow);
+ if not self.isBuildBlacklisted(oBuildExRet):
+ return oBuildExRet;
+
+ return None;
+
+ def isBuildBlacklisted(self, oBuildEx):
+ """
+ Checks if the given build is blacklisted
+ Returns True/False. May raise exception on database error.
+ """
+
+ asOsAgnosticArch = [];
+ asOsNoArch = [];
+ for sOsArch in oBuildEx.oCat.asOsArches:
+ asParts = sOsArch.split('.');
+ if len(asParts) != 2 or not asParts[0] or not asParts[1]:
+ raise self._oDb.integrityException('Bad build asOsArches value: %s (idBuild=%s idBuildCategory=%s)'
+ % (sOsArch, oBuildEx.idBuild, oBuildEx.idBuildCategory));
+ asOsNoArch.append(asParts[0] + '.noarch');
+ asOsNoArch.append('os-agnostic.' + asParts[1]);
+
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM BuildBlacklist\n'
+ 'WHERE BuildBlacklist.tsExpire > CURRENT_TIMESTAMP\n'
+ ' AND BuildBlacklist.tsEffective <= CURRENT_TIMESTAMP\n'
+ ' AND BuildBlacklist.sProduct = %s\n'
+ ' AND BuildBlacklist.sBranch = %s\n'
+ ' AND ( BuildBlacklist.asTypes is NULL\n'
+ ' OR %s = ANY(BuildBlacklist.asTypes))\n'
+ ' AND ( BuildBlacklist.asOsArches is NULL\n'
+ ' OR %s && BuildBlacklist.asOsArches\n' ## @todo check array rep! Need overload?
+ ' OR %s && BuildBlacklist.asOsArches\n'
+ ' OR %s && BuildBlacklist.asOsArches\n'
+ ' OR %s = ANY(BuildBlacklist.asOsArches))\n'
+ ' AND BuildBlacklist.iFirstRevision <= %s\n'
+ ' AND BuildBlacklist.iLastRevision >= %s\n'
+ , (oBuildEx.oCat.sProduct,
+ oBuildEx.oCat.sBranch,
+ oBuildEx.oCat.sType,
+ oBuildEx.oCat.asOsArches,
+ asOsAgnosticArch,
+ asOsNoArch,
+ 'os-agnostic.noarch',
+ oBuildEx.iRevision,
+ oBuildEx.iRevision,
+ ) );
+ return self._oDb.fetchOne()[0] > 0;
+
+
+ def getById(self, idBuild):
+ """
+ Get build record by its id
+ """
+ self._oDb.execute('SELECT Builds.*, BuildCategories.*\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE Builds.idBuild=%s\n'
+ ' AND Builds.idBuildCategory=BuildCategories.idBuildCategory\n'
+ ' AND Builds.tsExpire = \'infinity\'::TIMESTAMP\n', (idBuild,))
+
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise TMTooManyRows('Found more than one build with the same credentials. Database structure is corrupted.')
+ try:
+ return BuildDataEx().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+
+ def getAll(self, tsEffective = None):
+ """
+ Gets the list of all builds.
+ Returns an array of BuildDataEx instances.
+ """
+ if tsEffective is None:
+ self._oDb.execute('SELECT Builds.*, BuildCategories.*\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE Builds.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND Builds.idBuildCategory=BuildCategories.idBuildCategory')
+ else:
+ self._oDb.execute('SELECT Builds.*, BuildCategories.*\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE Builds.tsExpire > %s\n'
+ ' AND Builds.tsEffective <= %s'
+ ' AND Builds.idBuildCategory=BuildCategories.idBuildCategory'
+ , (tsEffective, tsEffective))
+ aoRet = []
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(BuildDataEx().initFromDbRow(aoRow))
+ return aoRet
+
+
+ def markDeletedByBinaries(self, sBinaries, fCommit = False):
+ """
+ Marks zero or more builds deleted given the build binaries.
+
+ Returns the number of affected builds.
+ """
+ # Fetch a list of affected build IDs (generally 1 build), and used the
+ # editEntry method to do the rest. This isn't 100% optimal, but it's
+ # short and simple, the main effort is anyway the first query.
+ self._oDb.execute('SELECT idBuild\n'
+ 'FROM Builds\n'
+ 'WHERE sBinaries = %s\n'
+ ' AND fBinariesDeleted = FALSE\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (sBinaries,));
+ aaoRows = self._oDb.fetchAll();
+ for aoRow in aaoRows:
+ oData = BuildData().initFromDbWithId(self._oDb, aoRow[0]);
+ assert not oData.fBinariesDeleted;
+ oData.fBinariesDeleted = True;
+ self.editEntry(oData, fCommit = False);
+ self._oDb.maybeCommit(fCommit);
+ return len(aaoRows);
+
+
+
+ #
+ # Internal helpers.
+ #
+
+ def _historizeBuild(self, idBuild, tsExpire = None):
+ """ Historizes the current entry for the specified build. """
+ if tsExpire is None:
+ self._oDb.execute('UPDATE Builds\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idBuild = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idBuild,));
+ else:
+ self._oDb.execute('UPDATE Builds\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idBuild = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (tsExpire, idBuild,));
+ return True;
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class BuildCategoryDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [BuildCategoryData(),];
+
+class BuildDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [BuildData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/buildblacklist.py b/src/VBox/ValidationKit/testmanager/core/buildblacklist.py
new file mode 100755
index 00000000..487fb9f0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/buildblacklist.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+# $Id: buildblacklist.py $
+
+"""
+Test Manager - Builds Blacklist.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelLogicBase, TMInvalidData, TMRowNotFound;
+
+
+class BuildBlacklistData(ModelDataBase):
+ """
+ Build Blacklist Data.
+ """
+
+ ksIdAttr = 'idBlacklisting';
+
+ ksParam_idBlacklisting = 'BuildBlacklist_idBlacklisting'
+ ksParam_tsEffective = 'BuildBlacklist_tsEffective'
+ ksParam_tsExpire = 'BuildBlacklist_tsExpire'
+ ksParam_uidAuthor = 'BuildBlacklist_uidAuthor'
+ ksParam_idFailureReason = 'BuildBlacklist_idFailureReason'
+ ksParam_sProduct = 'BuildBlacklist_sProduct'
+ ksParam_sBranch = 'BuildBlacklist_sBranch'
+ ksParam_asTypes = 'BuildBlacklist_asTypes'
+ ksParam_asOsArches = 'BuildBlacklist_asOsArches'
+ ksParam_iFirstRevision = 'BuildBlacklist_iFirstRevision'
+ ksParam_iLastRevision = 'BuildBlacklist_iLastRevision'
+
+ kasAllowNullAttributes = [ 'idBlacklisting',
+ 'tsEffective',
+ 'tsExpire',
+ 'uidAuthor',
+ 'asTypes',
+ 'asOsArches' ];
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idBlacklisting = None
+ self.tsEffective = None
+ self.tsExpire = None
+ self.uidAuthor = None
+ self.idFailureReason = None
+ self.sProduct = None
+ self.sBranch = None
+ self.asTypes = None
+ self.asOsArches = None
+ self.iFirstRevision = None
+ self.iLastRevision = None
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the data with a row from a SELECT * FROM BuildBlacklist.
+
+ Returns self. Raises exception if the row is None or otherwise invalid.
+ """
+
+ if aoRow is None:
+ raise TMRowNotFound('Build Blacklist item not found.')
+
+ self.idBlacklisting = aoRow[0]
+ self.tsEffective = aoRow[1]
+ self.tsExpire = aoRow[2]
+ self.uidAuthor = aoRow[3]
+ self.idFailureReason = aoRow[4]
+ self.sProduct = aoRow[5]
+ self.sBranch = aoRow[6]
+ self.asTypes = aoRow[7]
+ self.asOsArches = aoRow[8]
+ self.iFirstRevision = aoRow[9]
+ self.iLastRevision = aoRow[10]
+
+ return self;
+
+ def initFromDbWithId(self, oDb, idBlacklisting, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM BuildBlacklist\n'
+ 'WHERE idBlacklisting = %s\n'
+ , ( idBlacklisting,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idBlacklisting=%s not found (tsNow=%s sPeriodBack=%s)'
+ % (idBlacklisting, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+
+class BuildBlacklistLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Build Back List logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches Build Blacklist records.
+
+ Returns an array (list) of BuildBlacklistData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildBlacklist\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY idBlacklisting DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildBlacklist\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY idBlacklisting DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRows = []
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(BuildBlacklistData().initFromDbRow(aoRow))
+ return aoRows
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Adds a blacklisting to the database.
+ """
+ self._oDb.execute('INSERT INTO BuildBlacklist (\n'
+ ' uidAuthor,\n'
+ ' idFailureReason,\n'
+ ' sProduct,\n'
+ ' sBranch,\n'
+ ' asTypes,\n'
+ ' asOsArches,\n'
+ ' iFirstRevision,\n'
+ ' iLastRevision)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)'
+ , ( uidAuthor,
+ oData.idFailureReason,
+ oData.sProduct,
+ oData.sBranch,
+ oData.asTypes,
+ oData.asOsArches,
+ oData.iFirstRevision,
+ oData.iLastRevision,) );
+ self._oDb.maybeCommit(fCommit);
+ return True
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Modifies a blacklisting.
+ """
+
+ #
+ # Validate inputs and read in the old(/current) data.
+ #
+ assert isinstance(oData, BuildBlacklistData);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
+
+ oOldData = BuildBlacklistData().initFromDbWithId(self._oDb, oData.idBlacklisting);
+
+ #
+ # Update the data that needs updating.
+ #
+ if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
+ self._historizeEntry(oData.idBlacklisting, None);
+ self._readdEntry(uidAuthor, oData, None);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def removeEntry(self, uidAuthor, idBlacklisting, fCascade = False, fCommit = False):
+ """
+ Deletes a test group.
+ """
+ _ = fCascade; # Not applicable.
+
+ oData = BuildBlacklistData().initFromDbWithId(self._oDb, idBlacklisting);
+
+ (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
+ if oData.tsEffective not in (tsCur, tsCurMinusOne):
+ self._historizeEntry(idBlacklisting, tsCurMinusOne);
+ self._readdEntry(uidAuthor, oData, tsCurMinusOne);
+ self._historizeEntry(idBlacklisting);
+ self._oDb.execute('UPDATE BuildBlacklist\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idBlacklisting = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idBlacklisting,));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def cachedLookup(self, idBlacklisting):
+ """
+ Looks up the most recent BuildBlacklistData object for idBlacklisting
+ via an object cache.
+
+ Returns a shared BuildBlacklistData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('BuildBlacklistData');
+ oEntry = self.dCache.get(idBlacklisting, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildBlacklist\n'
+ 'WHERE idBlacklisting = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idBlacklisting, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildBlacklist\n'
+ 'WHERE idBlacklisting = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idBlacklisting, ));
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBlacklisting));
+
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ oEntry = BuildBlacklistData();
+ oEntry.initFromDbRow(aaoRow);
+ self.dCache[idBlacklisting] = oEntry;
+ return oEntry;
+
+
+ #
+ # Helpers.
+ #
+
+ def _historizeEntry(self, idBlacklisting, tsExpire = None):
+ """
+ Historizes the current entry for the given backlisting.
+ """
+ if tsExpire is None:
+ tsExpire = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('UPDATE BuildBlacklist\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idBlacklisting = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( tsExpire, idBlacklisting, ));
+ return True;
+
+ def _readdEntry(self, uidAuthor, oData, tsEffective = None):
+ """
+ Re-adds the BuildBlacklist entry. Used by editEntry and removeEntry.
+ """
+ if tsEffective is None:
+ tsEffective = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('INSERT INTO BuildBlacklist (\n'
+ ' uidAuthor,\n'
+ ' tsEffective,\n'
+ ' idBlacklisting,\n'
+ ' idFailureReason,\n'
+ ' sProduct,\n'
+ ' sBranch,\n'
+ ' asTypes,\n'
+ ' asOsArches,\n'
+ ' iFirstRevision,\n'
+ ' iLastRevision)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)\n'
+ , ( uidAuthor,
+ tsEffective,
+ oData.idBlacklisting,
+ oData.idFailureReason,
+ oData.sProduct,
+ oData.sBranch,
+ oData.asTypes,
+ oData.asOsArches,
+ oData.iFirstRevision,
+ oData.iLastRevision,) );
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/buildsource.py b/src/VBox/ValidationKit/testmanager/core/buildsource.py
new file mode 100755
index 00000000..782b5ee0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/buildsource.py
@@ -0,0 +1,524 @@
+# -*- coding: utf-8 -*-
+# $Id: buildsource.py $
+
+"""
+Test Manager - Build Sources.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from common import utils;
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMRowAlreadyExists, \
+ TMRowInUse, TMInvalidData, TMRowNotFound;
+from testmanager.core import coreconsts;
+
+
+class BuildSourceData(ModelDataBase):
+ """
+ A build source.
+ """
+
+ ksIdAttr = 'idBuildSrc';
+
+ ksParam_idBuildSrc = 'BuildSource_idBuildSrc';
+ ksParam_tsEffective = 'BuildSource_tsEffective';
+ ksParam_tsExpire = 'BuildSource_tsExpire';
+ ksParam_uidAuthor = 'BuildSource_uidAuthor';
+ ksParam_sName = 'BuildSource_sName';
+ ksParam_sDescription = 'BuildSource_sDescription';
+ ksParam_sProduct = 'BuildSource_sProduct';
+ ksParam_sBranch = 'BuildSource_sBranch';
+ ksParam_asTypes = 'BuildSource_asTypes';
+ ksParam_asOsArches = 'BuildSource_asOsArches';
+ ksParam_iFirstRevision = 'BuildSource_iFirstRevision';
+ ksParam_iLastRevision = 'BuildSource_iLastRevision';
+ ksParam_cSecMaxAge = 'BuildSource_cSecMaxAge';
+
+ kasAllowNullAttributes = [ 'idBuildSrc', 'tsEffective', 'tsExpire', 'uidAuthor', 'sDescription', 'asTypes',
+ 'asOsArches', 'iFirstRevision', 'iLastRevision', 'cSecMaxAge' ];
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idBuildSrc = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.sName = None;
+ self.sDescription = None;
+ self.sProduct = None;
+ self.sBranch = None;
+ self.asTypes = None;
+ self.asOsArches = None;
+ self.iFirstRevision = None;
+ self.iLastRevision = None;
+ self.cSecMaxAge = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the object from a SELECT * FROM BuildSources row.
+ Returns self. Raises exception if aoRow is None.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Build source not found.');
+
+ self.idBuildSrc = aoRow[0];
+ self.tsEffective = aoRow[1];
+ self.tsExpire = aoRow[2];
+ self.uidAuthor = aoRow[3];
+ self.sName = aoRow[4];
+ self.sDescription = aoRow[5];
+ self.sProduct = aoRow[6];
+ self.sBranch = aoRow[7];
+ self.asTypes = aoRow[8];
+ self.asOsArches = aoRow[9];
+ self.iFirstRevision = aoRow[10];
+ self.iLastRevision = aoRow[11];
+ self.cSecMaxAge = aoRow[12];
+ return self;
+
+ def initFromDbWithId(self, oDb, idBuildSrc, tsNow = None, sPeriodBack = None):
+ """
+ Initialize from the database, given the ID of a row.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM BuildSources\n'
+ 'WHERE idBuildSrc = %s\n'
+ , ( idBuildSrc,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idBuildSrc=%s not found (tsNow=%s sPeriodBack=%s)' % (idBuildSrc, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ # Handle asType and asOsArches specially.
+ if sAttr == 'sType':
+ (oNewValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue,
+ aoNilValues, fAllowNull, oDb);
+ if sError is None:
+ if not self.asTypes:
+ oNewValue = None;
+ else:
+ for sType in oNewValue:
+ if len(sType) < 2 or sType.lower() != sType:
+ if sError is None: sError = '';
+ else: sError += ', ';
+ sError += 'invalid value "%s"' % (sType,);
+
+ elif sAttr == 'asOsArches':
+ (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ asValidValues = coreconsts.g_kasOsDotCpusAll);
+ if sError is not None and oNewValue is not None:
+ oNewValue = sorted(oNewValue); # Must be sorted!
+
+ elif sAttr == 'cSecMaxAge' and oValue not in aoNilValues: # Allow human readable interval formats.
+ (oNewValue, sError) = utils.parseIntervalSeconds(oValue);
+ else:
+ return ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+
+ return (oNewValue, sError);
+
+class BuildSourceLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Build source database logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ #
+ # Standard methods.
+ #
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches build sources.
+
+ Returns an array (list) of BuildSourceData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildSources\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY idBuildSrc DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildSources\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY idBuildSrc DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRows = []
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(BuildSourceData().initFromDbRow(aoRow))
+ return aoRows
+
+ def fetchForCombo(self):
+ """Fetch data which is aimed to be passed to HTML form"""
+ self._oDb.execute('SELECT idBuildSrc, sName, sProduct\n'
+ 'FROM BuildSources\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY idBuildSrc DESC\n')
+ asRet = self._oDb.fetchAll();
+ asRet.insert(0, (-1, 'None', 'None'));
+ return asRet;
+
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Add a new build source to the database.
+ """
+
+ #
+ # Validate the input.
+ #
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dErrors:
+ raise TMInvalidData('addEntry invalid input: %s' % (dErrors,));
+ self._assertUnique(oData, None);
+
+ #
+ # Add it.
+ #
+ self._oDb.execute('INSERT INTO BuildSources (\n'
+ ' uidAuthor,\n'
+ ' sName,\n'
+ ' sDescription,\n'
+ ' sProduct,\n'
+ ' sBranch,\n'
+ ' asTypes,\n'
+ ' asOsArches,\n'
+ ' iFirstRevision,\n'
+ ' iLastRevision,\n'
+ ' cSecMaxAge)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)\n'
+ , ( uidAuthor,
+ oData.sName,
+ oData.sDescription,
+ oData.sProduct,
+ oData.sBranch,
+ oData.asTypes,
+ oData.asOsArches,
+ oData.iFirstRevision,
+ oData.iLastRevision,
+ oData.cSecMaxAge, ));
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Modifies a build source.
+ """
+
+ #
+ # Validate the input and read the old entry.
+ #
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('addEntry invalid input: %s' % (dErrors,));
+ self._assertUnique(oData, oData.idBuildSrc);
+ oOldData = BuildSourceData().initFromDbWithId(self._oDb, oData.idBuildSrc);
+
+ #
+ # Make the changes (if something actually changed).
+ #
+ if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
+ self._historizeBuildSource(oData.idBuildSrc);
+ self._oDb.execute('INSERT INTO BuildSources (\n'
+ ' uidAuthor,\n'
+ ' idBuildSrc,\n'
+ ' sName,\n'
+ ' sDescription,\n'
+ ' sProduct,\n'
+ ' sBranch,\n'
+ ' asTypes,\n'
+ ' asOsArches,\n'
+ ' iFirstRevision,\n'
+ ' iLastRevision,\n'
+ ' cSecMaxAge)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)\n'
+ , ( uidAuthor,
+ oData.idBuildSrc,
+ oData.sName,
+ oData.sDescription,
+ oData.sProduct,
+ oData.sBranch,
+ oData.asTypes,
+ oData.asOsArches,
+ oData.iFirstRevision,
+ oData.iLastRevision,
+ oData.cSecMaxAge, ));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def removeEntry(self, uidAuthor, idBuildSrc, fCascade = False, fCommit = False):
+ """
+ Deletes a build sources.
+ """
+
+ #
+ # Check cascading.
+ #
+ if fCascade is not True:
+ self._oDb.execute('SELECT idSchedGroup, sName\n'
+ 'FROM SchedGroups\n'
+ 'WHERE idBuildSrc = %s\n'
+ ' OR idBuildSrcTestSuite = %s\n'
+ , (idBuildSrc, idBuildSrc,));
+ if self._oDb.getRowCount() > 0:
+ asGroups = [];
+ for aoRow in self._oDb.fetchAll():
+ asGroups.append('%s (#%d)' % (aoRow[1], aoRow[0]));
+ raise TMRowInUse('Build source #%d is used by one or more scheduling groups: %s'
+ % (idBuildSrc, ', '.join(asGroups),));
+ else:
+ self._oDb.execute('UPDATE SchedGroups\n'
+ 'SET idBuildSrc = NULL\n'
+ 'WHERE idBuildSrc = %s'
+ , ( idBuildSrc,));
+ self._oDb.execute('UPDATE SchedGroups\n'
+ 'SET idBuildSrcTestSuite = NULL\n'
+ 'WHERE idBuildSrcTestSuite = %s'
+ , ( idBuildSrc,));
+
+ #
+ # Do the job.
+ #
+ self._historizeBuildSource(idBuildSrc, None);
+ _ = uidAuthor; ## @todo record deleter.
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def cachedLookup(self, idBuildSrc):
+ """
+ Looks up the most recent BuildSourceData object for idBuildSrc
+ via an object cache.
+
+ Returns a shared BuildSourceData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('BuildSourceData');
+ oEntry = self.dCache.get(idBuildSrc, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildSources\n'
+ 'WHERE idBuildSrc = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idBuildSrc, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildSources\n'
+ 'WHERE idBuildSrc = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idBuildSrc, ));
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBuildSrc));
+
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ oEntry = BuildSourceData();
+ oEntry.initFromDbRow(aaoRow);
+ self.dCache[idBuildSrc] = oEntry;
+ return oEntry;
+
+ #
+ # Other methods.
+ #
+
+ def openBuildCursor(self, oBuildSource, sOs, sCpuArch, tsNow):
+ """
+ Opens a cursor (SELECT) using the criteria found in the build source
+ and the given OS.CPUARCH.
+
+ Returns database cursor. May raise exception on bad input or logic error.
+
+ Used by SchedulerBase.
+ """
+
+ oCursor = self._oDb.openCursor();
+
+ #
+ # Construct the extra conditionals.
+ #
+ sExtraConditions = '';
+
+ # Types
+ if oBuildSource.asTypes is not None and oBuildSource.asTypes:
+ if len(oBuildSource.asTypes) == 1:
+ sExtraConditions += oCursor.formatBindArgs(' AND BuildCategories.sType = %s', (oBuildSource.asTypes[0],));
+ else:
+ sExtraConditions += oCursor.formatBindArgs(' AND BuildCategories.sType IN (%s', (oBuildSource.asTypes[0],))
+ for i in range(1, len(oBuildSource.asTypes) - 1):
+ sExtraConditions += oCursor.formatBindArgs(', %s', (oBuildSource.asTypes[i],));
+ sExtraConditions += oCursor.formatBindArgs(', %s)\n', (oBuildSource.asTypes[-1],));
+
+ # BuildSource OSes.ARCHes. (Paranoia: use a dictionary to avoid duplicate values.)
+ if oBuildSource.asOsArches is not None and oBuildSource.asOsArches:
+ sExtraConditions += oCursor.formatBindArgs(' AND BuildCategories.asOsArches && %s', (oBuildSource.asOsArches,));
+
+ # TestBox OSes.ARCHes. (Paranoia: use a dictionary to avoid duplicate values.)
+ dOsDotArches = {};
+ dOsDotArches[sOs + '.' + sCpuArch] = 1;
+ dOsDotArches[sOs + '.' + coreconsts.g_ksCpuArchAgnostic] = 1;
+ dOsDotArches[coreconsts.g_ksOsAgnostic + '.' + sCpuArch] = 1;
+ dOsDotArches[coreconsts.g_ksOsDotArchAgnostic] = 1;
+ sExtraConditions += oCursor.formatBindArgs(' AND BuildCategories.asOsArches && %s', (list(dOsDotArches.keys()),));
+
+ # Revision range.
+ if oBuildSource.iFirstRevision is not None:
+ sExtraConditions += oCursor.formatBindArgs(' AND Builds.iRevision >= %s\n', (oBuildSource.iFirstRevision,));
+ if oBuildSource.iLastRevision is not None:
+ sExtraConditions += oCursor.formatBindArgs(' AND Builds.iRevision <= %s\n', (oBuildSource.iLastRevision,));
+
+ # Max age.
+ if oBuildSource.cSecMaxAge is not None:
+ sExtraConditions += oCursor.formatBindArgs(' AND Builds.tsCreated >= (%s - \'%s seconds\'::INTERVAL)\n',
+ (tsNow, oBuildSource.cSecMaxAge,));
+
+ #
+ # Execute the query.
+ #
+ oCursor.execute('SELECT Builds.*, BuildCategories.*,\n'
+ ' EXISTS( SELECT tsExpire\n'
+ ' FROM BuildBlacklist\n'
+ ' WHERE BuildBlacklist.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND BuildBlacklist.sProduct = %s\n'
+ ' AND BuildBlacklist.sBranch = %s\n'
+ ' AND BuildBlacklist.iFirstRevision <= Builds.iRevision\n'
+ ' AND BuildBlacklist.iLastRevision >= Builds.iRevision ) AS fMaybeBlacklisted\n'
+ 'FROM Builds, BuildCategories\n'
+ 'WHERE Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ ' AND Builds.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND Builds.tsEffective <= %s\n'
+ ' AND Builds.fBinariesDeleted is FALSE\n'
+ ' AND BuildCategories.sProduct = %s\n'
+ ' AND BuildCategories.sBranch = %s\n'
+ + sExtraConditions +
+ 'ORDER BY Builds.idBuild DESC\n'
+ 'LIMIT 256\n'
+ , ( oBuildSource.sProduct, oBuildSource.sBranch,
+ tsNow, oBuildSource.sProduct, oBuildSource.sBranch,));
+
+ return oCursor;
+
+
+ def getById(self, idBuildSrc):
+ """Get Build Source data by idBuildSrc"""
+
+ self._oDb.execute('SELECT *\n'
+ 'FROM BuildSources\n'
+ 'WHERE tsExpire = \'infinity\'::timestamp\n'
+ ' AND idBuildSrc = %s;', (idBuildSrc,))
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise self._oDb.integrityException(
+ 'Found more than one build sources with the same credentials. Database structure is corrupted.')
+ try:
+ return BuildSourceData().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+ #
+ # Internal helpers.
+ #
+
+ def _assertUnique(self, oData, idBuildSrcIgnore):
+ """ Checks that the build source name is unique, raises exception if it isn't. """
+ self._oDb.execute('SELECT idBuildSrc\n'
+ 'FROM BuildSources\n'
+ 'WHERE sName = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ + ('' if idBuildSrcIgnore is None else ' AND idBuildSrc <> %d\n' % (idBuildSrcIgnore,))
+ , ( oData.sName, ))
+ if self._oDb.getRowCount() > 0:
+ raise TMRowAlreadyExists('A build source with name "%s" already exist.' % (oData.sName,));
+ return True;
+
+
+ def _historizeBuildSource(self, idBuildSrc, tsExpire = None):
+ """ Historizes the current build source entry. """
+ if tsExpire is None:
+ self._oDb.execute('UPDATE BuildSources\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idBuildSrc = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( idBuildSrc, ));
+ else:
+ self._oDb.execute('UPDATE BuildSources\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idBuildSrc = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( tsExpire, idBuildSrc, ));
+ return True;
+
+
+
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class BuildSourceDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [BuildSourceData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/coreconsts.py b/src/VBox/ValidationKit/testmanager/core/coreconsts.py
new file mode 100644
index 00000000..bbdc7712
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/coreconsts.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# $Id: coreconsts.py $
+
+"""
+Test Manager - Test Manager Constants (without a more appropriate home).
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+## OS agnostic.
+g_ksOsAgnostic = 'os-agnostic';
+## All known OSes, except the agnostic one.
+# See KBUILD_OSES in kBuild/header.kmk for reference.
+g_kasOses = ['darwin', 'dos', 'dragonfly', 'freebsd', 'haiku', 'l4', 'linux', 'netbsd', 'nt', 'openbsd', 'os2',
+ 'solaris', 'win'];
+## All known OSes, including the agnostic one.
+# See KBUILD_OSES in kBuild/header.kmk for reference.
+g_kasOsesAll = g_kasOses + [g_ksOsAgnostic,];
+
+
+## Architecture agnostic.
+g_ksCpuArchAgnostic = 'noarch';
+## All known CPU architectures, except the agnostic one.
+# See KBUILD_ARCHES in kBuild/header.kmk for reference.
+g_kasCpuArches = ['amd64', 'x86', 'sparc32', 'sparc64', 's390', 's390x', 'ppc32', 'ppc64', 'mips32', 'mips64', 'ia64',
+ 'hppa32', 'hppa64', 'arm', 'alpha'];
+## All known CPU architectures, except the agnostic one.
+# See KBUILD_ARCHES in kBuild/header.kmk for reference.
+g_kasCpuArchesAll = g_kasCpuArches + [g_ksCpuArchAgnostic,];
+
+## All known build types
+# See KBUILD_TYPE in kBuild/header.kmk for reference.
+# @note 'blessed' is a special type used for release builds that has been notarized
+# or attestation signed by the OS vendor.
+g_kasBuildTypesAll = [ 'release', 'strict', 'profile', 'debug', 'asan', 'blessed' ];
+
+## OS and CPU architecture agnostic.
+g_ksOsDotArchAgnostic = 'os-agnostic.noarch';
+## Combinations of all OSes and CPU architectures, except the two agnostic ones.
+# We do some of them by hand to avoid offering too many choices.
+g_kasOsDotCpus = \
+[
+ 'darwin.amd64', 'darwin.x86', 'darwin.ppc32', 'darwin.ppc64', 'darwin.arm',
+ 'dos.x86',
+ 'dragonfly.amd64', 'dragonfly.x86',
+ 'freebsd.amd64', 'freebsd.x86', 'freebsd.sparc64', 'freebsd.ia64', 'freebsd.ppc32', 'freebsd.ppc64', 'freebsd.arm',
+ 'freebsd.mips32', 'freebsd.mips64',
+ 'haiku.amd64', 'haiku.x86',
+ 'l4.amd64', 'l4.x86', 'l4.ppc32', 'l4.ppc64', 'l4.arm',
+ 'nt.amd64', 'nt.x86', 'nt.arm', 'nt.ia64', 'nt.mips32', 'nt.ppc32', 'nt.alpha',
+ 'win.amd64', 'win.x86', 'win.arm', 'win.ia64', 'win.mips32', 'win.ppc32', 'win.alpha',
+ 'os2.x86',
+ 'solaris.amd64', 'solaris.x86', 'solaris.sparc32', 'solaris.sparc64',
+];
+for sOs in g_kasOses:
+ if sOs not in ['darwin', 'dos', 'dragonfly', 'freebsd', 'haiku', 'l4', 'nt', 'win', 'os2', 'solaris']:
+ for sArch in g_kasCpuArches:
+ g_kasOsDotCpus.append(sOs + '.' + sArch);
+g_kasOsDotCpus.sort();
+
+## Combinations of all OSes and CPU architectures, including the two agnostic ones.
+g_kasOsDotCpusAll = [g_ksOsDotArchAgnostic]
+g_kasOsDotCpusAll.extend(g_kasOsDotCpus);
+for sOs in g_kasOsesAll:
+ g_kasOsDotCpusAll.append(sOs + '.' + g_ksCpuArchAgnostic);
+for sArch in g_kasCpuArchesAll:
+ g_kasOsDotCpusAll.append(g_ksOsAgnostic + '.' + sArch);
+g_kasOsDotCpusAll.sort();
+
diff --git a/src/VBox/ValidationKit/testmanager/core/db.py b/src/VBox/ValidationKit/testmanager/core/db.py
new file mode 100755
index 00000000..915336c3
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/db.py
@@ -0,0 +1,745 @@
+# -*- coding: utf-8 -*-
+# $Id: db.py $
+
+"""
+Test Manager - Database Interface.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import datetime;
+import os;
+import sys;
+import psycopg2; # pylint: disable=import-error
+import psycopg2.extensions; # pylint: disable=import-error
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager import config;
+
+# Fix psycho unicode handling in psycopg2 with python 2.x.
+if sys.version_info[0] < 3:
+ psycopg2.extensions.register_type(psycopg2.extensions.UNICODE);
+ psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY);
+else:
+ unicode = str; # pylint: disable=redefined-builtin,invalid-name
+
+
+
+def isDbTimestampInfinity(tsValue):
+ """
+ Checks if tsValue is an infinity timestamp.
+ """
+ ## @todo improve this test...
+ return tsValue.year >= 9999;
+
+def isDbTimestamp(oValue):
+ """
+ Checks if oValue is a DB timestamp object.
+ """
+ if isinstance(oValue, datetime.datetime):
+ return True;
+ if utils.isString(oValue):
+ ## @todo detect strings as well.
+ return False;
+ return getattr(oValue, 'pydatetime', None) is not None;
+
+def dbTimestampToDatetime(oValue):
+ """
+ Converts a database timestamp to a datetime instance.
+ """
+ if isinstance(oValue, datetime.datetime):
+ return oValue;
+ if utils.isString(oValue):
+ return utils.parseIsoTimestamp(oValue);
+ return oValue.pydatetime();
+
+def dbTimestampToZuluDatetime(oValue):
+ """
+ Converts a database timestamp to a zulu datetime instance.
+ """
+ tsValue = dbTimestampToDatetime(oValue);
+
+ class UTC(datetime.tzinfo):
+ """UTC TZ Info Class"""
+ def utcoffset(self, _):
+ return datetime.timedelta(0);
+ def tzname(self, _):
+ return "UTC";
+ def dst(self, _):
+ return datetime.timedelta(0);
+ if tsValue.tzinfo is not None:
+ tsValue = tsValue.astimezone(UTC());
+ else:
+ tsValue = tsValue.replace(tzinfo=UTC());
+ return tsValue;
+
+def dbTimestampPythonNow():
+ """
+ Gets the current python timestamp in a database compatible way.
+ """
+ return dbTimestampToZuluDatetime(datetime.datetime.utcnow());
+
+def dbOneTickIntervalString():
+ """
+ Returns the interval string for one tick.
+
+ Mogrify the return value into the SQL:
+ "... %s::INTERVAL ..."
+ or
+ "INTERVAL %s"
+ The completed SQL will contain the necessary ticks.
+ """
+ return '1 microsecond';
+
+def dbTimestampMinusOneTick(oValue):
+ """
+ Returns a new timestamp that's one tick before the given one.
+ """
+ oValue = dbTimestampToZuluDatetime(oValue);
+ return oValue - datetime.timedelta(microseconds = 1);
+
+def dbTimestampPlusOneTick(oValue):
+ """
+ Returns a new timestamp that's one tick after the given one.
+ """
+ oValue = dbTimestampToZuluDatetime(oValue);
+ return oValue + datetime.timedelta(microseconds = 1);
+
+def isDbInterval(oValue):
+ """
+ Checks if oValue is a DB interval object.
+ """
+ if isinstance(oValue, datetime.timedelta):
+ return True;
+ return False;
+
+
+class TMDatabaseIntegrityException(Exception):
+ """
+ Herolds a database integrity error up the callstack.
+
+ Do NOT use directly, only thru TMDatabaseConnection.integrityException.
+ Otherwise, we won't be able to log the issue.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TMDatabaseCursor(object):
+ """ Cursor wrapper class. """
+
+ def __init__(self, oDb, oCursor):
+ self._oDb = oDb;
+ self._oCursor = oCursor;
+
+ def execute(self, sOperation, aoArgs = None):
+ """ See TMDatabaseConnection.execute()"""
+ return self._oDb.executeInternal(self._oCursor, sOperation, aoArgs, utils.getCallerName());
+
+ def callProc(self, sProcedure, aoArgs = None):
+ """ See TMDatabaseConnection.callProc()"""
+ return self._oDb.callProcInternal(self._oCursor, sProcedure, aoArgs, utils.getCallerName());
+
+ def insertList(self, sInsertSql, aoList, fnEntryFmt):
+ """ See TMDatabaseConnection.insertList. """
+ return self._oDb.insertListInternal(self._oCursor, sInsertSql, aoList, fnEntryFmt, utils.getCallerName());
+
+ def fetchOne(self):
+ """Wrapper around Psycopg2.cursor.fetchone."""
+ return self._oCursor.fetchone();
+
+ def fetchMany(self, cRows = None):
+ """Wrapper around Psycopg2.cursor.fetchmany."""
+ return self._oCursor.fetchmany(cRows if cRows is not None else self._oCursor.arraysize);
+
+ def fetchAll(self):
+ """Wrapper around Psycopg2.cursor.fetchall."""
+ return self._oCursor.fetchall();
+
+ def getRowCount(self):
+ """Wrapper around Psycopg2.cursor.rowcount."""
+ return self._oCursor.rowcount;
+
+ def formatBindArgs(self, sStatement, aoArgs):
+ """Wrapper around Psycopg2.cursor.mogrify."""
+ oRet = self._oCursor.mogrify(sStatement, aoArgs);
+ if sys.version_info[0] >= 3 and not isinstance(oRet, str):
+ oRet = oRet.decode('utf-8');
+ return oRet;
+
+ def copyExpert(self, sSqlCopyStmt, oFile, cbBuf = 8192):
+ """ See TMDatabaseConnection.copyExpert()"""
+ return self._oCursor.copy_expert(sSqlCopyStmt, oFile, cbBuf);
+
+ @staticmethod
+ def isTsInfinity(tsValue):
+ """ Checks if tsValue is an infinity timestamp. """
+ return isDbTimestampInfinity(tsValue);
+
+
+class TMDatabaseConnection(object):
+ """
+ Test Manager Database Access class.
+
+ This class contains no logic, just raw access abstraction and utilities,
+ as well as some debug help and some statistics.
+ """
+
+ def __init__(self, fnDPrint = None, oSrvGlue = None):
+ """
+ Database connection wrapper.
+ The fnDPrint is for debug logging of all database activity.
+
+ Raises an exception on failure.
+ """
+
+ sAppName = '%s-%s' % (os.getpid(), os.path.basename(sys.argv[0]),)
+ if len(sAppName) >= 64:
+ sAppName = sAppName[:64];
+ os.environ['PGAPPNAME'] = sAppName;
+
+ dArgs = \
+ { \
+ 'database': config.g_ksDatabaseName,
+ 'user': config.g_ksDatabaseUser,
+ 'password': config.g_ksDatabasePassword,
+ # 'application_name': sAppName, - Darn stale debian! :/
+ };
+ if config.g_ksDatabaseAddress is not None:
+ dArgs['host'] = config.g_ksDatabaseAddress;
+ if config.g_ksDatabasePort is not None:
+ dArgs['port'] = config.g_ksDatabasePort;
+ self._oConn = psycopg2.connect(**dArgs); # pylint: disable=star-args
+ self._oConn.set_client_encoding('UTF-8');
+ self._oCursor = self._oConn.cursor();
+ self._oExplainConn = None;
+ self._oExplainCursor = None;
+ if config.g_kfWebUiSqlTraceExplain and config.g_kfWebUiSqlTrace:
+ self._oExplainConn = psycopg2.connect(**dArgs); # pylint: disable=star-args
+ self._oExplainConn.set_client_encoding('UTF-8');
+ self._oExplainCursor = self._oExplainConn.cursor();
+ self._fTransaction = False;
+ self._tsCurrent = None;
+ self._tsCurrentMinusOne = None;
+
+ assert self.isAutoCommitting() is False;
+
+ # Debug and introspection.
+ self._fnDPrint = fnDPrint;
+ self._aoTraceBack = [];
+
+ # Exception class handles.
+ self.oXcptError = psycopg2.Error;
+
+ if oSrvGlue is not None:
+ oSrvGlue.registerDebugInfoCallback(self.debugInfoCallback);
+
+ # Object caches (used by database logic classes).
+ self.ddCaches = {};
+
+ def isAutoCommitting(self):
+ """ Work around missing autocommit attribute in older versions."""
+ return getattr(self._oConn, 'autocommit', False);
+
+ def close(self):
+ """
+ Closes the connection and renders all cursors useless.
+ """
+ if self._oCursor is not None:
+ self._oCursor.close();
+ self._oCursor = None;
+
+ if self._oConn is not None:
+ self._oConn.close();
+ self._oConn = None;
+
+ if self._oExplainCursor is not None:
+ self._oExplainCursor.close();
+ self._oExplainCursor = None;
+
+ if self._oExplainConn is not None:
+ self._oExplainConn.close();
+ self._oExplainConn = None;
+
+
+ def _startedTransaction(self):
+ """
+ Called to work the _fTransaction and related variables when starting
+ a transaction.
+ """
+ self._fTransaction = True;
+ self._tsCurrent = None;
+ self._tsCurrentMinusOne = None;
+ return None;
+
+ def _endedTransaction(self):
+ """
+ Called to work the _fTransaction and related variables when ending
+ a transaction.
+ """
+ self._fTransaction = False;
+ self._tsCurrent = None;
+ self._tsCurrentMinusOne = None;
+ return None;
+
+ def begin(self):
+ """
+ Currently just for marking where a transaction starts in the code.
+ """
+ assert self._oConn is not None;
+ assert self.isAutoCommitting() is False;
+ self._aoTraceBack.append([utils.timestampNano(), 'START TRANSACTION', 0, 0, utils.getCallerName(), None]);
+ self._startedTransaction();
+ return True;
+
+ def commit(self, sCallerName = None):
+ """ Wrapper around Psycopg2.connection.commit."""
+ assert self._fTransaction is True;
+
+ nsStart = utils.timestampNano();
+ oRc = self._oConn.commit();
+ cNsElapsed = utils.timestampNano() - nsStart;
+
+ if sCallerName is None:
+ sCallerName = utils.getCallerName();
+ self._aoTraceBack.append([nsStart, 'COMMIT', cNsElapsed, 0, sCallerName, None]);
+ self._endedTransaction();
+ return oRc;
+
+ def maybeCommit(self, fCommit):
+ """
+ Commits if fCommit is True.
+ Returns True if committed, False if not.
+ """
+ if fCommit is True:
+ self.commit(utils.getCallerName());
+ return True;
+ return False;
+
+ def rollback(self):
+ """ Wrapper around Psycopg2.connection.rollback."""
+ nsStart = utils.timestampNano();
+ oRc = self._oConn.rollback();
+ cNsElapsed = utils.timestampNano() - nsStart;
+
+ self._aoTraceBack.append([nsStart, 'ROLLBACK', cNsElapsed, 0, utils.getCallerName(), None]);
+ self._endedTransaction();
+ return oRc;
+
+ #
+ # Internal cursor workers.
+ #
+
+ def executeInternal(self, oCursor, sOperation, aoArgs, sCallerName):
+ """
+ Execute a query or command.
+
+ Mostly a wrapper around the psycopg2 cursor method with the same name,
+ but collect data for traceback.
+ """
+ if aoArgs is not None:
+ sBound = oCursor.mogrify(unicode(sOperation), aoArgs);
+ elif sOperation.find('%') < 0:
+ sBound = oCursor.mogrify(unicode(sOperation), []);
+ else:
+ sBound = unicode(sOperation);
+
+ if sys.version_info[0] >= 3 and not isinstance(sBound, str):
+ sBound = sBound.decode('utf-8'); # pylint: disable=redefined-variable-type
+
+ aasExplain = None;
+ if self._oExplainCursor is not None and not sBound.startswith('DROP'):
+ try:
+ if config.g_kfWebUiSqlTraceExplainTiming:
+ self._oExplainCursor.execute('EXPLAIN (ANALYZE, BUFFERS, COSTS, VERBOSE, TIMING) ' + sBound);
+ else:
+ self._oExplainCursor.execute('EXPLAIN (ANALYZE, BUFFERS, COSTS, VERBOSE) ' + sBound);
+ except Exception as oXcpt:
+ aasExplain = [ ['Explain exception: '], [str(oXcpt)]];
+ try: self._oExplainConn.rollback();
+ except: pass;
+ else:
+ aasExplain = self._oExplainCursor.fetchall();
+
+ nsStart = utils.timestampNano();
+ try:
+ oRc = oCursor.execute(sBound);
+ except Exception as oXcpt:
+ cNsElapsed = utils.timestampNano() - nsStart;
+ self._aoTraceBack.append([nsStart, 'oXcpt=%s; Statement: %s' % (oXcpt, sBound), cNsElapsed, 0, sCallerName, None]);
+ if self._fnDPrint is not None:
+ self._fnDPrint('db::execute %u ns, caller %s: oXcpt=%s; Statement: %s'
+ % (cNsElapsed, sCallerName, oXcpt, sBound));
+ raise;
+ cNsElapsed = utils.timestampNano() - nsStart;
+
+ if self._fTransaction is False and not self.isAutoCommitting(): # Even SELECTs starts transactions with psycopg2, see FAQ.
+ self._aoTraceBack.append([nsStart, '[START TRANSACTION]', 0, 0, sCallerName, None]);
+ self._startedTransaction();
+ self._aoTraceBack.append([nsStart, sBound, cNsElapsed, oCursor.rowcount, sCallerName, aasExplain]);
+ if self._fnDPrint is not None:
+ self._fnDPrint('db::execute %u ns, caller %s: "\n%s"' % (cNsElapsed, sCallerName, sBound));
+ if self.isAutoCommitting():
+ self._aoTraceBack.append([nsStart, '[AUTO COMMIT]', 0, 0, sCallerName, None]);
+
+ return oRc;
+
+ def callProcInternal(self, oCursor, sProcedure, aoArgs, sCallerName):
+ """
+ Call a stored procedure.
+
+ Mostly a wrapper around the psycopg2 cursor method 'callproc', but
+ collect data for traceback.
+ """
+ if aoArgs is None:
+ aoArgs = [];
+
+ nsStart = utils.timestampNano();
+ try:
+ oRc = oCursor.callproc(sProcedure, aoArgs);
+ except Exception as oXcpt:
+ cNsElapsed = utils.timestampNano() - nsStart;
+ self._aoTraceBack.append([nsStart, 'oXcpt=%s; Calling: %s(%s)' % (oXcpt, sProcedure, aoArgs),
+ cNsElapsed, 0, sCallerName, None]);
+ if self._fnDPrint is not None:
+ self._fnDPrint('db::callproc %u ns, caller %s: oXcpt=%s; Calling: %s(%s)'
+ % (cNsElapsed, sCallerName, oXcpt, sProcedure, aoArgs));
+ raise;
+ cNsElapsed = utils.timestampNano() - nsStart;
+
+ if self._fTransaction is False and not self.isAutoCommitting(): # Even SELECTs starts transactions with psycopg2, see FAQ.
+ self._aoTraceBack.append([nsStart, '[START TRANSACTION]', 0, 0, sCallerName, None]);
+ self._startedTransaction();
+ self._aoTraceBack.append([nsStart, '%s(%s)' % (sProcedure, aoArgs), cNsElapsed, oCursor.rowcount, sCallerName, None]);
+ if self._fnDPrint is not None:
+ self._fnDPrint('db::callproc %u ns, caller %s: "%s(%s)"' % (cNsElapsed, sCallerName, sProcedure, aoArgs));
+ if self.isAutoCommitting():
+ self._aoTraceBack.append([nsStart, '[AUTO COMMIT]', 0, 0, sCallerName, sCallerName, None]);
+
+ return oRc;
+
+ def insertListInternal(self, oCursor, sInsertSql, aoList, fnEntryFmt, sCallerName):
+ """
+ Optimizes the insertion of a list of values.
+ """
+ oRc = None;
+ asValues = [];
+ for aoEntry in aoList:
+ asValues.append(fnEntryFmt(aoEntry));
+ if len(asValues) > 256:
+ oRc = self.executeInternal(oCursor, sInsertSql + 'VALUES' + ', '.join(asValues), None, sCallerName);
+ asValues = [];
+ if asValues:
+ oRc = self.executeInternal(oCursor, sInsertSql + 'VALUES' + ', '.join(asValues), None, sCallerName);
+ return oRc
+
+ def _fetchOne(self, oCursor):
+ """Wrapper around Psycopg2.cursor.fetchone."""
+ oRow = oCursor.fetchone()
+ if self._fnDPrint is not None:
+ self._fnDPrint('db:fetchOne returns: %s' % (oRow,));
+ return oRow;
+
+ def _fetchMany(self, oCursor, cRows):
+ """Wrapper around Psycopg2.cursor.fetchmany."""
+ return oCursor.fetchmany(cRows if cRows is not None else oCursor.arraysize);
+
+ def _fetchAll(self, oCursor):
+ """Wrapper around Psycopg2.cursor.fetchall."""
+ return oCursor.fetchall()
+
+ def _getRowCountWorker(self, oCursor):
+ """Wrapper around Psycopg2.cursor.rowcount."""
+ return oCursor.rowcount;
+
+
+ #
+ # Default cursor access.
+ #
+
+ def execute(self, sOperation, aoArgs = None):
+ """
+ Execute a query or command.
+
+ Mostly a wrapper around the psycopg2 cursor method with the same name,
+ but collect data for traceback.
+ """
+ return self.executeInternal(self._oCursor, sOperation, aoArgs, utils.getCallerName());
+
+ def callProc(self, sProcedure, aoArgs = None):
+ """
+ Call a stored procedure.
+
+ Mostly a wrapper around the psycopg2 cursor method 'callproc', but
+ collect data for traceback.
+ """
+ return self.callProcInternal(self._oCursor, sProcedure, aoArgs, utils.getCallerName());
+
+ def insertList(self, sInsertSql, aoList, fnEntryFmt):
+ """
+ Optimizes the insertion of a list of values.
+ """
+ return self.insertListInternal(self._oCursor, sInsertSql, aoList, fnEntryFmt, utils.getCallerName());
+
+ def fetchOne(self):
+ """Wrapper around Psycopg2.cursor.fetchone."""
+ return self._oCursor.fetchone();
+
+ def fetchMany(self, cRows = None):
+ """Wrapper around Psycopg2.cursor.fetchmany."""
+ return self._oCursor.fetchmany(cRows if cRows is not None else self._oCursor.arraysize);
+
+ def fetchAll(self):
+ """Wrapper around Psycopg2.cursor.fetchall."""
+ return self._oCursor.fetchall();
+
+ def getRowCount(self):
+ """Wrapper around Psycopg2.cursor.rowcount."""
+ return self._oCursor.rowcount;
+
+ def formatBindArgs(self, sStatement, aoArgs):
+ """Wrapper around Psycopg2.cursor.mogrify."""
+ oRet = self._oCursor.mogrify(sStatement, aoArgs);
+ if sys.version_info[0] >= 3 and not isinstance(oRet, str):
+ oRet = oRet.decode('utf-8');
+ return oRet;
+
+ def copyExpert(self, sSqlCopyStmt, oFile, cbBuf = 8192):
+ """ Wrapper around Psycopg2.cursor.copy_expert. """
+ return self._oCursor.copy_expert(sSqlCopyStmt, oFile, cbBuf);
+
+ def getCurrentTimestamps(self):
+ """
+ Returns the current timestamp and the current timestamp minus one tick.
+ This will start a transaction if necessary.
+ """
+ if self._tsCurrent is None:
+ self.execute('SELECT CURRENT_TIMESTAMP, CURRENT_TIMESTAMP - INTERVAL \'1 microsecond\'');
+ (self._tsCurrent, self._tsCurrentMinusOne) = self.fetchOne();
+ return (self._tsCurrent, self._tsCurrentMinusOne);
+
+ def getCurrentTimestamp(self):
+ """
+ Returns the current timestamp.
+ This will start a transaction if necessary.
+ """
+ if self._tsCurrent is None:
+ self.getCurrentTimestamps();
+ return self._tsCurrent;
+
+ def getCurrentTimestampMinusOne(self):
+ """
+ Returns the current timestamp minus one tick.
+ This will start a transaction if necessary.
+ """
+ if self._tsCurrentMinusOne is None:
+ self.getCurrentTimestamps();
+ return self._tsCurrentMinusOne;
+
+
+ #
+ # Additional cursors.
+ #
+ def openCursor(self):
+ """
+ Opens a new cursor (TMDatabaseCursor).
+ """
+ oCursor = self._oConn.cursor();
+ return TMDatabaseCursor(self, oCursor);
+
+ #
+ # Cache support.
+ #
+ def getCache(self, sType):
+ """ Returns the cache dictionary for this data type. """
+ dRet = self.ddCaches.get(sType, None);
+ if dRet is None:
+ dRet = {};
+ self.ddCaches[sType] = dRet;
+ return dRet;
+
+
+ #
+ # Utilities.
+ #
+
+ @staticmethod
+ def isTsInfinity(tsValue):
+ """ Checks if tsValue is an infinity timestamp. """
+ return isDbTimestampInfinity(tsValue);
+
+ #
+ # Error stuff.
+ #
+ def integrityException(self, sMessage):
+ """
+ Database integrity reporter and exception factory.
+ Returns an TMDatabaseIntegrityException which the caller can raise.
+ """
+ ## @todo Create a new database connection and log the issue in the SystemLog table.
+ ## Alternatively, rollback whatever is going on and do it using the current one.
+ return TMDatabaseIntegrityException(sMessage);
+
+
+ #
+ # Debugging.
+ #
+
+ def dprint(self, sText):
+ """
+ Debug output.
+ """
+ if not self._fnDPrint:
+ return False;
+ self._fnDPrint(sText);
+ return True;
+
+ def debugHtmlReport(self, tsStart = 0):
+ """
+ Used to get a SQL activity dump as HTML, usually for WuiBase._sDebug.
+ """
+ cNsElapsed = 0;
+ for aEntry in self._aoTraceBack:
+ cNsElapsed += aEntry[2];
+
+ sDebug = '<h3>SQL Debug Log (total time %s ns):</h3>\n' \
+ '<table class="tmsqltable">\n' \
+ ' <tr>\n' \
+ ' <th>No.</th>\n' \
+ ' <th>Timestamp (ns)</th>\n' \
+ ' <th>Elapsed (ns)</th>\n' \
+ ' <th>Rows Returned</th>\n' \
+ ' <th>Command</th>\n' \
+ ' <th>Caller</th>\n' \
+ ' </tr>\n' \
+ % (utils.formatNumber(cNsElapsed, '&nbsp;'),);
+
+ iEntry = 0;
+ for aEntry in self._aoTraceBack:
+ iEntry += 1;
+ sDebug += ' <tr>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td><pre>%s</pre></td>\n' \
+ ' <td>%s</td>\n' \
+ ' </tr>\n' \
+ % (iEntry,
+ utils.formatNumber(aEntry[0] - tsStart, '&nbsp;'),
+ utils.formatNumber(aEntry[2], '&nbsp;'),
+ utils.formatNumber(aEntry[3], '&nbsp;'),
+ webutils.escapeElem(aEntry[1]),
+ webutils.escapeElem(aEntry[4]),
+ );
+ if aEntry[5] is not None:
+ sDebug += ' <tr>\n' \
+ ' <td colspan="6"><pre style="white-space: pre-wrap;">%s</pre></td>\n' \
+ ' </tr>\n' \
+ % (webutils.escapeElem('\n'.join([aoRow[0] for aoRow in aEntry[5]])),);
+
+ sDebug += '</table>';
+ return sDebug;
+
+ def debugTextReport(self, tsStart = 0):
+ """
+ Used to get a SQL activity dump as text.
+ """
+ cNsElapsed = 0;
+ for aEntry in self._aoTraceBack:
+ cNsElapsed += aEntry[2];
+
+ sHdr = 'SQL Debug Log (total time %s ns)' % (utils.formatNumber(cNsElapsed),);
+ sDebug = sHdr + '\n' + '-' * len(sHdr) + '\n';
+
+ iEntry = 0;
+ for aEntry in self._aoTraceBack:
+ iEntry += 1;
+ sHdr = 'Query #%s Timestamp: %s ns Elapsed: %s ns Rows: %s Caller: %s' \
+ % ( iEntry,
+ utils.formatNumber(aEntry[0] - tsStart),
+ utils.formatNumber(aEntry[2]),
+ utils.formatNumber(aEntry[3]),
+ aEntry[4], );
+ sDebug += '\n' + sHdr + '\n' + '-' * len(sHdr) + '\n';
+
+ sDebug += aEntry[1];
+ if sDebug[-1] != '\n':
+ sDebug += '\n';
+
+ if aEntry[5] is not None:
+ sDebug += 'Explain:\n' \
+ ' %s\n' \
+ % ( '\n'.join([aoRow[0] for aoRow in aEntry[5]]),);
+
+ return sDebug;
+
+ def debugInfoCallback(self, oGlue, fHtml):
+ """ Called back by the glue code on error. """
+ oGlue.write('\n');
+ if not fHtml: oGlue.write(self.debugTextReport());
+ else: oGlue.write(self.debugHtmlReport());
+ oGlue.write('\n');
+ return True;
+
+ def debugEnableExplain(self):
+ """ Enabled explain. """
+ if self._oExplainConn is None:
+ dArgs = \
+ { \
+ 'database': config.g_ksDatabaseName,
+ 'user': config.g_ksDatabaseUser,
+ 'password': config.g_ksDatabasePassword,
+ # 'application_name': sAppName, - Darn stale debian! :/
+ };
+ if config.g_ksDatabaseAddress is not None:
+ dArgs['host'] = config.g_ksDatabaseAddress;
+ if config.g_ksDatabasePort is not None:
+ dArgs['port'] = config.g_ksDatabasePort;
+ self._oExplainConn = psycopg2.connect(**dArgs); # pylint: disable=star-args
+ self._oExplainCursor = self._oExplainConn.cursor();
+ return True;
+
+ def debugDisableExplain(self):
+ """ Disables explain. """
+ self._oExplainCursor = None;
+ self._oExplainConn = None
+ return True;
+
+ def debugIsExplainEnabled(self):
+ """ Check if explaining of SQL statements is enabled. """
+ return self._oExplainConn is not None;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/dbobjcache.py b/src/VBox/ValidationKit/testmanager/core/dbobjcache.py
new file mode 100755
index 00000000..b059dccd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/dbobjcache.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+# $Id: dbobjcache.py $
+
+"""
+Test Manager - Database object cache.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.core.base import ModelLogicBase;
+
+
+class DatabaseObjCache(ModelLogicBase):
+ """
+ Database object cache.
+
+ This is mainly for reports and test results where we wish to get further
+ information on a data series or similar. The cache should reduce database
+ lookups as well as pyhon memory footprint.
+
+ Note! Dependecies are imported when needed to avoid potential cylic dependency issues.
+ """
+
+ ## @name Cache object types.
+ ## @{
+ ksObjType_TestResultStrTab_idStrName = 0;
+ ksObjType_BuildCategory_idBuildCategory = 1;
+ ksObjType_TestBox_idTestBox = 2;
+ ksObjType_TestBox_idGenTestBox = 3;
+ ksObjType_TestCase_idTestCase = 4;
+ ksObjType_TestCase_idGenTestCase = 5;
+ ksObjType_TestCaseArgs_idTestCaseArgs = 6;
+ ksObjType_TestCaseArgs_idGenTestCaseArgs = 7;
+ ksObjType_VcsRevision_sRepository_iRevision = 8;
+ ksObjType_End = 9;
+ ## @}
+
+ def __init__(self, oDb, tsNow = None, sPeriodBack = None, cHoursBack = None):
+ ModelLogicBase.__init__(self, oDb);
+
+ self.tsNow = tsNow;
+ self.sPeriodBack = sPeriodBack;
+ if sPeriodBack is None and cHoursBack is not None:
+ self.sPeriodBack = '%u hours' % cHoursBack;
+
+ self._adCache = (
+ {}, {}, {}, {},
+ {}, {}, {}, {},
+ {},
+ );
+ assert(len(self._adCache) == self.ksObjType_End);
+
+ def _handleDbException(self):
+ """ Deals with database exceptions. """
+ #self._oDb.rollback();
+ return False;
+
+ def getTestResultString(self, idStrName):
+ """ Gets a string from the TestResultStrTab. """
+ sRet = self._adCache[self.ksObjType_TestResultStrTab_idStrName].get(idStrName);
+ if sRet is None:
+ # Load cache entry.
+ self._oDb.execute('SELECT sValue FROM TestResultStrTab WHERE idStr = %s', (idStrName,));
+ sRet = self._oDb.fetchOne()[0];
+ self._adCache[self.ksObjType_TestResultStrTab_idStrName][idStrName] = sRet
+ return sRet;
+
+ def getBuildCategory(self, idBuildCategory):
+ """ Gets the corresponding BuildCategoryData object. """
+ oRet = self._adCache[self.ksObjType_BuildCategory_idBuildCategory].get(idBuildCategory);
+ if oRet is None:
+ # Load cache entry.
+ from testmanager.core.build import BuildCategoryData;
+ oRet = BuildCategoryData();
+ try: oRet.initFromDbWithId(self._oDb, idBuildCategory);
+ except: self._handleDbException(); raise;
+ self._adCache[self.ksObjType_BuildCategory_idBuildCategory][idBuildCategory] = oRet;
+ return oRet;
+
+ def getTestBox(self, idTestBox):
+ """ Gets the corresponding TestBoxData object. """
+ oRet = self._adCache[self.ksObjType_TestBox_idTestBox].get(idTestBox);
+ if oRet is None:
+ # Load cache entry.
+ from testmanager.core.testbox import TestBoxData;
+ oRet = TestBoxData();
+ try: oRet.initFromDbWithId(self._oDb, idTestBox, self.tsNow, self.sPeriodBack);
+ except: self._handleDbException(); raise;
+ else: self._adCache[self.ksObjType_TestBox_idGenTestBox][oRet.idGenTestBox] = oRet;
+ self._adCache[self.ksObjType_TestBox_idTestBox][idTestBox] = oRet;
+ return oRet;
+
+ def getTestCase(self, idTestCase):
+ """ Gets the corresponding TestCaseData object. """
+ oRet = self._adCache[self.ksObjType_TestCase_idTestCase].get(idTestCase);
+ if oRet is None:
+ # Load cache entry.
+ from testmanager.core.testcase import TestCaseData;
+ oRet = TestCaseData();
+ try: oRet.initFromDbWithId(self._oDb, idTestCase, self.tsNow, self.sPeriodBack);
+ except: self._handleDbException(); raise;
+ else: self._adCache[self.ksObjType_TestCase_idGenTestCase][oRet.idGenTestCase] = oRet;
+ self._adCache[self.ksObjType_TestCase_idTestCase][idTestCase] = oRet;
+ return oRet;
+
+ def getTestCaseArgs(self, idTestCaseArgs):
+ """ Gets the corresponding TestCaseArgsData object. """
+ oRet = self._adCache[self.ksObjType_TestCaseArgs_idTestCaseArgs].get(idTestCaseArgs);
+ if oRet is None:
+ # Load cache entry.
+ from testmanager.core.testcaseargs import TestCaseArgsData;
+ oRet = TestCaseArgsData();
+ try: oRet.initFromDbWithId(self._oDb, idTestCaseArgs, self.tsNow, self.sPeriodBack);
+ except: self._handleDbException(); raise;
+ else: self._adCache[self.ksObjType_TestCaseArgs_idGenTestCaseArgs][oRet.idGenTestCaseArgs] = oRet;
+ self._adCache[self.ksObjType_TestCaseArgs_idTestCaseArgs][idTestCaseArgs] = oRet;
+ return oRet;
+
+ def preloadVcsRevInfo(self, sRepository, aiRevisions):
+ """
+ Preloads VCS revision information.
+ ASSUMES aiRevisions does not contain duplicate keys.
+ """
+ from testmanager.core.vcsrevisions import VcsRevisionData;
+ dRepo = self._adCache[self.ksObjType_VcsRevision_sRepository_iRevision].get(sRepository);
+ if dRepo is None:
+ dRepo = {};
+ self._adCache[self.ksObjType_VcsRevision_sRepository_iRevision][sRepository] = dRepo;
+ aiFiltered = aiRevisions;
+ else:
+ aiFiltered = [];
+ for iRevision in aiRevisions:
+ if iRevision not in dRepo:
+ aiFiltered.append(iRevision);
+ if aiFiltered:
+ self._oDb.execute('SELECT *\n'
+ 'FROM VcsRevisions\n'
+ 'WHERE sRepository = %s\n'
+ ' AND iRevision IN (' + ','.join([str(i) for i in aiFiltered]) + ')'
+ , ( sRepository, ));
+ for aoRow in self._oDb.fetchAll():
+ oInfo = VcsRevisionData().initFromDbRow(aoRow);
+ dRepo[oInfo.iRevision] = oInfo;
+ return True;
+
+ def getVcsRevInfo(self, sRepository, iRevision):
+ """
+ Gets the corresponding VcsRevisionData object.
+ May return a default (all NULLs) VcsRevisionData object if the revision
+ information isn't available in the database yet.
+ """
+ dRepo = self._adCache[self.ksObjType_VcsRevision_sRepository_iRevision].get(sRepository);
+ if dRepo is not None:
+ oRet = dRepo.get(iRevision);
+ else:
+ dRepo = {};
+ self._adCache[self.ksObjType_VcsRevision_sRepository_iRevision][sRepository] = dRepo;
+ oRet = None;
+ if oRet is None:
+ from testmanager.core.vcsrevisions import VcsRevisionLogic;
+ oRet = VcsRevisionLogic(self._oDb).tryFetch(sRepository, iRevision);
+ if oRet is None:
+ from testmanager.core.vcsrevisions import VcsRevisionData;
+ oRet = VcsRevisionData();
+ dRepo[iRevision] = oRet;
+ return oRet;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/failurecategory.py b/src/VBox/ValidationKit/testmanager/core/failurecategory.py
new file mode 100755
index 00000000..3da0f84b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/failurecategory.py
@@ -0,0 +1,392 @@
+# -*- coding: utf-8 -*-
+# $Id: failurecategory.py $
+
+"""
+Test Manager - Failure Categories.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard Python imports.
+import sys;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowInUse, TMInvalidData, TMRowNotFound, \
+ ChangeLogEntry, AttributeChangeEntry;
+from testmanager.core.useraccount import UserAccountLogic;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ xrange = range; # pylint: disable=redefined-builtin,invalid-name
+
+
+class FailureCategoryData(ModelDataBase):
+ """
+ Failure Category Data.
+ """
+
+ ksIdAttr = 'idFailureCategory';
+
+ ksParam_idFailureCategory = 'FailureCategory_idFailureCategory'
+ ksParam_tsEffective = 'FailureCategory_tsEffective'
+ ksParam_tsExpire = 'FailureCategory_tsExpire'
+ ksParam_uidAuthor = 'FailureCategory_uidAuthor'
+ ksParam_sShort = 'FailureCategory_sShort'
+ ksParam_sFull = 'FailureCategory_sFull'
+
+ kasAllowNullAttributes = [ 'idFailureCategory', 'tsEffective', 'tsExpire', 'uidAuthor' ]
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+
+ self.idFailureCategory = None
+ self.tsEffective = None
+ self.tsExpire = None
+ self.uidAuthor = None
+ self.sShort = None
+ self.sFull = None
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the data with a row from a SELECT * FROM FailureCategoryes.
+
+ Returns self. Raises exception if the row is None or otherwise invalid.
+ """
+
+ if aoRow is None:
+ raise TMRowNotFound('Failure Category not found.');
+
+ self.idFailureCategory = aoRow[0]
+ self.tsEffective = aoRow[1]
+ self.tsExpire = aoRow[2]
+ self.uidAuthor = aoRow[3]
+ self.sShort = aoRow[4]
+ self.sFull = aoRow[5]
+
+ return self
+
+ def initFromDbWithId(self, oDb, idFailureCategory, tsNow = None, sPeriodBack = None):
+ """
+ Initialize from the database, given the ID of a row.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM FailureCategories\n'
+ 'WHERE idFailureCategory = %s\n'
+ , ( idFailureCategory,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idFailureCategory=%s not found (tsNow=%s sPeriodBack=%s)'
+ % (idFailureCategory, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+
+class FailureCategoryLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Failure Category logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches Failure Category records.
+
+ Returns an array (list) of FailureCategoryData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureCategories\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY idFailureCategory ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureCategories\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY idFailureCategory ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRows = []
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(FailureCategoryData().initFromDbRow(aoRow))
+ return aoRows
+
+
+ def fetchForChangeLog(self, idFailureCategory, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals
+ """
+ Fetches change log entries for a failure reason.
+
+ Returns an array of ChangeLogEntry instance and an indicator whether
+ there are more entries.
+ Raises exception on error.
+ """
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+
+ # 1. Get a list of the relevant changes.
+ self._oDb.execute('SELECT * FROM FailureCategories WHERE idFailureCategory = %s AND tsEffective <= %s\n'
+ 'ORDER BY tsEffective DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , ( idFailureCategory, tsNow, cMaxRows + 1, iStart, ));
+ aoRows = [];
+ for aoChange in self._oDb.fetchAll():
+ aoRows.append(FailureCategoryData().initFromDbRow(aoChange));
+
+ # 2. Calculate the changes.
+ aoEntries = [];
+ for i in xrange(0, len(aoRows) - 1):
+ oNew = aoRows[i];
+ oOld = aoRows[i + 1];
+
+ aoChanges = [];
+ for sAttr in oNew.getDataAttributes():
+ if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+ oOldAttr = getattr(oOld, sAttr);
+ oNewAttr = getattr(oNew, sAttr);
+ if oOldAttr != oNewAttr:
+ aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+
+ aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
+
+ # If we're at the end of the log, add the initial entry.
+ if len(aoRows) <= cMaxRows and aoRows:
+ oNew = aoRows[-1];
+ aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
+
+ return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
+
+
+ def getFailureCategoriesForCombo(self, tsEffective = None):
+ """
+ Gets the list of Failure Categories for a combo box.
+ Returns an array of (value [idFailureCategory], drop-down-name [sShort],
+ hover-text [sFull]) tuples.
+ """
+ if tsEffective is None:
+ self._oDb.execute('SELECT idFailureCategory, sShort, sFull\n'
+ 'FROM FailureCategories\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sShort')
+ else:
+ self._oDb.execute('SELECT idFailureCategory, sShort, sFull\n'
+ 'FROM FailureCategories\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY sShort'
+ , (tsEffective, tsEffective))
+ return self._oDb.fetchAll()
+
+
+ def getById(self, idFailureCategory):
+ """Get Failure Category data by idFailureCategory"""
+
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureCategories\n'
+ 'WHERE tsExpire = \'infinity\'::timestamp\n'
+ ' AND idFailureCategory = %s;', (idFailureCategory,))
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise self._oDb.integrityException(
+ 'Found more than one failure categories with the same credentials. Database structure is corrupted.')
+ try:
+ return FailureCategoryData().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Add a failure reason category.
+ """
+ #
+ # Validate inputs and read in the old(/current) data.
+ #
+ assert isinstance(oData, FailureCategoryData);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dErrors:
+ raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
+
+ #
+ # Add the record.
+ #
+ self._readdEntry(uidAuthor, oData);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Modifies a failure reason category.
+ """
+
+ #
+ # Validate inputs and read in the old(/current) data.
+ #
+ assert isinstance(oData, FailureCategoryData);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
+
+ oOldData = FailureCategoryData().initFromDbWithId(self._oDb, oData.idFailureCategory);
+
+ #
+ # Update the data that needs updating.
+ #
+ if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
+ self._historizeEntry(oData.idFailureCategory);
+ self._readdEntry(uidAuthor, oData);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def removeEntry(self, uidAuthor, idFailureCategory, fCascade = False, fCommit = False):
+ """
+ Deletes a failure reason category.
+ """
+ _ = fCascade; # too complicated for now.
+
+ #
+ # Check whether it's being used by other tables and bitch if it is .
+ # We currently do not implement cascading.
+ #
+ self._oDb.execute('SELECT CONCAT(idFailureReason, \' - \', sShort)\n'
+ 'FROM FailureReasons\n'
+ 'WHERE idFailureCategory = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idFailureCategory,));
+ aaoRows = self._oDb.fetchAll();
+ if aaoRows:
+ raise TMRowInUse('Cannot remove failure reason category %u because its being used by: %s'
+ % (idFailureCategory, ', '.join(aoRow[0] for aoRow in aaoRows),));
+
+ #
+ # Do the job.
+ #
+ oData = FailureCategoryData().initFromDbWithId(self._oDb, idFailureCategory);
+ (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
+ if oData.tsEffective not in (tsCur, tsCurMinusOne):
+ self._historizeEntry(idFailureCategory, tsCurMinusOne);
+ self._readdEntry(uidAuthor, oData, tsCurMinusOne);
+ self._historizeEntry(idFailureCategory);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def cachedLookup(self, idFailureCategory):
+ """
+ Looks up the most recent FailureCategoryData object for idFailureCategory
+ via an object cache.
+
+ Returns a shared FailureCategoryData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('FailureCategory');
+
+ oEntry = self.dCache.get(idFailureCategory, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureCategories\n'
+ 'WHERE idFailureCategory = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idFailureCategory, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureCategories\n'
+ 'WHERE idFailureCategory = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idFailureCategory, ));
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureCategory));
+
+ if self._oDb.getRowCount() == 1:
+ oEntry = FailureCategoryData().initFromDbRow(self._oDb.fetchOne());
+ self.dCache[idFailureCategory] = oEntry;
+ return oEntry;
+
+
+ #
+ # Helpers.
+ #
+
+ def _readdEntry(self, uidAuthor, oData, tsEffective = None):
+ """
+ Re-adds the FailureCategories entry. Used by addEntry, editEntry and removeEntry.
+ """
+ if tsEffective is None:
+ tsEffective = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('INSERT INTO FailureCategories (\n'
+ ' uidAuthor,\n'
+ ' tsEffective,\n'
+ ' idFailureCategory,\n'
+ ' sShort,\n'
+ ' sFull)\n'
+ 'VALUES (%s, %s, '
+ + ('DEFAULT' if oData.idFailureCategory is None else str(oData.idFailureCategory))
+ + ', %s, %s)\n'
+ , ( uidAuthor,
+ tsEffective,
+ oData.sShort,
+ oData.sFull,) );
+ return True;
+
+
+ def _historizeEntry(self, idFailureCategory, tsExpire = None):
+ """ Historizes the current entry. """
+ if tsExpire is None:
+ tsExpire = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('UPDATE FailureCategories\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idFailureCategory = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (tsExpire, idFailureCategory,));
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/failurereason.py b/src/VBox/ValidationKit/testmanager/core/failurereason.py
new file mode 100755
index 00000000..0d9294b0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/failurereason.py
@@ -0,0 +1,580 @@
+# -*- coding: utf-8 -*-
+# $Id: failurereason.py $
+
+"""
+Test Manager - Failure Reasons.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard Python imports.
+import sys;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse, \
+ AttributeChangeEntry, ChangeLogEntry;
+from testmanager.core.useraccount import UserAccountLogic;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ xrange = range; # pylint: disable=redefined-builtin,invalid-name
+
+
+class FailureReasonData(ModelDataBase):
+ """
+ Failure Reason Data.
+ """
+
+ ksIdAttr = 'idFailureReason';
+
+ ksParam_idFailureReason = 'FailureReasonData_idFailureReason'
+ ksParam_tsEffective = 'FailureReasonData_tsEffective'
+ ksParam_tsExpire = 'FailureReasonData_tsExpire'
+ ksParam_uidAuthor = 'FailureReasonData_uidAuthor'
+ ksParam_idFailureCategory = 'FailureReasonData_idFailureCategory'
+ ksParam_sShort = 'FailureReasonData_sShort'
+ ksParam_sFull = 'FailureReasonData_sFull'
+ ksParam_iTicket = 'FailureReasonData_iTicket'
+ ksParam_asUrls = 'FailureReasonData_asUrls'
+
+ kasAllowNullAttributes = [ 'idFailureReason', 'tsEffective', 'tsExpire',
+ 'uidAuthor', 'iTicket', 'asUrls' ]
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+
+ self.idFailureReason = None
+ self.tsEffective = None
+ self.tsExpire = None
+ self.uidAuthor = None
+ self.idFailureCategory = None
+ self.sShort = None
+ self.sFull = None
+ self.iTicket = None
+ self.asUrls = None
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the data with a row from a SELECT * FROM FailureReasons.
+
+ Returns self. Raises exception if the row is None or otherwise invalid.
+ """
+
+ if aoRow is None:
+ raise TMRowNotFound('Failure Reason not found.');
+
+ self.idFailureReason = aoRow[0]
+ self.tsEffective = aoRow[1]
+ self.tsExpire = aoRow[2]
+ self.uidAuthor = aoRow[3]
+ self.idFailureCategory = aoRow[4]
+ self.sShort = aoRow[5]
+ self.sFull = aoRow[6]
+ self.iTicket = aoRow[7]
+ self.asUrls = aoRow[8]
+
+ return self;
+
+ def initFromDbWithId(self, oDb, idFailureReason, tsNow = None, sPeriodBack = None):
+ """
+ Initialize from the database, given the ID of a row.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM FailureReasons\n'
+ 'WHERE idFailureReason = %s\n'
+ , ( idFailureReason,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idFailureReason=%s not found (tsNow=%s sPeriodBack=%s)'
+ % (idFailureReason, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+
+class FailureReasonDataEx(FailureReasonData):
+ """
+ Failure Reason Data, extended version that includes the category.
+ """
+
+ def __init__(self):
+ FailureReasonData.__init__(self);
+ self.oCategory = None;
+ self.oAuthor = None;
+
+ def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic):
+ """
+ Re-initializes the data with a row from a SELECT * FROM FailureReasons.
+
+ Returns self. Raises exception if the row is None or otherwise invalid.
+ """
+
+ self.initFromDbRow(aoRow);
+ self.oCategory = oCategoryLogic.cachedLookup(self.idFailureCategory);
+ self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor);
+
+ return self;
+
+
+class FailureReasonLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Failure Reason logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+ self.dCacheNameAndCat = None;
+ self.oCategoryLogic = None;
+ self.oUserAccountLogic = None;
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches Failure Category records.
+
+ Returns an array (list) of FailureReasonDataEx items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+ self._ensureCachesPresent();
+
+ if tsNow is None:
+ self._oDb.execute('SELECT FailureReasons.*,\n'
+ ' FailureCategories.sShort AS sCategory\n'
+ 'FROM FailureReasons,\n'
+ ' FailureCategories\n'
+ 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
+ ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sCategory ASC, sShort ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT FailureReasons.*,\n'
+ ' FailureCategories.sShort AS sCategory\n'
+ 'FROM FailureReasons,\n'
+ ' FailureCategories\n'
+ 'WHERE FailureReasons.tsExpire > %s\n'
+ ' AND FailureReasons.tsEffective <= %s\n'
+ ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
+ ' AND FailureReasons.tsExpire > %s\n'
+ ' AND FailureReasons.tsEffective <= %s\n'
+ 'ORDER BY sCategory ASC, sShort ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRows = []
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
+ return aoRows
+
+ def fetchForListingInCategory(self, iStart, cMaxRows, tsNow, idFailureCategory, aiSortColumns = None):
+ """
+ Fetches Failure Category records.
+
+ Returns an array (list) of FailureReasonDataEx items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+ self._ensureCachesPresent();
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureReasons\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND idFailureCategory = %s\n'
+ 'ORDER BY sShort ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , ( idFailureCategory, cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureReasons\n'
+ 'WHERE idFailureCategory = %s\n'
+ ' AND tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY sShort ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , ( idFailureCategory, tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRows = []
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
+ return aoRows
+
+
+ def fetchForSheriffByNamedCategory(self, sFailureCategory):
+ """
+ Fetches the short names of the reasons in the named category.
+
+ Returns array of strings.
+ Raises exception on error.
+ """
+ self._oDb.execute('SELECT FailureReasons.sShort\n'
+ 'FROM FailureReasons,\n'
+ ' FailureCategories\n'
+ 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
+ ' AND FailureCategories.sShort = %s\n'
+ 'ORDER BY FailureReasons.sShort ASC\n'
+ , ( sFailureCategory,));
+ return [aoRow[0] for aoRow in self._oDb.fetchAll()];
+
+
+ def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None):
+ """
+ Gets the list of Failure Reasons for a combo box.
+ Returns an array of (value [idFailureReason], drop-down-name [sShort],
+ hover-text [sFull]) tuples.
+ """
+ if tsEffective is None:
+ self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
+ 'FROM FailureReasons fr,\n'
+ ' FailureCategories fc\n'
+ 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
+ ' AND fr.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND fc.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sComboText')
+ else:
+ self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
+ 'FROM FailureReasons fr,\n'
+ ' FailureCategories fc\n'
+ 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
+ ' AND fr.tsExpire > %s\n'
+ ' AND fr.tsEffective <= %s\n'
+ ' AND fc.tsExpire > %s\n'
+ ' AND fc.tsEffective <= %s\n'
+ 'ORDER BY sComboText'
+ , (tsEffective, tsEffective, tsEffective, tsEffective));
+ aoRows = self._oDb.fetchAll();
+ return [(-1, sFirstEntry, '')] + aoRows;
+
+
+ def fetchForChangeLog(self, idFailureReason, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals
+ """
+ Fetches change log entries for a failure reason.
+
+ Returns an array of ChangeLogEntry instance and an indicator whether
+ there are more entries.
+ Raises exception on error.
+ """
+ self._ensureCachesPresent();
+
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+
+ # 1. Get a list of the relevant changes.
+ self._oDb.execute('SELECT * FROM FailureReasons WHERE idFailureReason = %s AND tsEffective <= %s\n'
+ 'ORDER BY tsEffective DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , ( idFailureReason, tsNow, cMaxRows + 1, iStart, ));
+ aoRows = [];
+ for aoChange in self._oDb.fetchAll():
+ aoRows.append(FailureReasonData().initFromDbRow(aoChange));
+
+ # 2. Calculate the changes.
+ aoEntries = [];
+ for i in xrange(0, len(aoRows) - 1):
+ oNew = aoRows[i];
+ oOld = aoRows[i + 1];
+
+ aoChanges = [];
+ for sAttr in oNew.getDataAttributes():
+ if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+ oOldAttr = getattr(oOld, sAttr);
+ oNewAttr = getattr(oNew, sAttr);
+ if oOldAttr != oNewAttr:
+ if sAttr == 'idFailureCategory':
+ oCat = self.oCategoryLogic.cachedLookup(oOldAttr);
+ if oCat is not None:
+ oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
+ oCat = self.oCategoryLogic.cachedLookup(oNewAttr);
+ if oCat is not None:
+ oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
+ aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+
+ aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
+
+ # If we're at the end of the log, add the initial entry.
+ if len(aoRows) <= cMaxRows and aoRows:
+ oNew = aoRows[-1];
+ aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
+
+ return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
+
+
+ def getById(self, idFailureReason):
+ """Get Failure Reason data by idFailureReason"""
+
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureReasons\n'
+ 'WHERE tsExpire = \'infinity\'::timestamp\n'
+ ' AND idFailureReason = %s;', (idFailureReason,))
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise self._oDb.integrityException(
+ 'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
+ try:
+ return FailureReasonData().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Add a failure reason.
+ """
+ #
+ # Validate.
+ #
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dErrors:
+ raise TMInvalidData('addEntry invalid input: %s' % (dErrors,));
+
+ #
+ # Add the record.
+ #
+ self._readdEntry(uidAuthor, oData);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Modifies a failure reason.
+ """
+
+ #
+ # Validate inputs and read in the old(/current) data.
+ #
+ assert isinstance(oData, FailureReasonData);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
+
+ oOldData = FailureReasonData().initFromDbWithId(self._oDb, oData.idFailureReason);
+
+ #
+ # Update the data that needs updating.
+ #
+ if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
+ self._historizeEntry(oData.idFailureReason);
+ self._readdEntry(uidAuthor, oData);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def removeEntry(self, uidAuthor, idFailureReason, fCascade = False, fCommit = False):
+ """
+ Deletes a failure reason.
+ """
+ _ = fCascade; # too complicated for now.
+
+ #
+ # Check whether it's being used by other tables and bitch if it is .
+ # We currently do not implement cascading.
+ #
+ self._oDb.execute('SELECT CONCAT(idBlacklisting, \' - blacklisting\')\n'
+ 'FROM BuildBlacklist\n'
+ 'WHERE idFailureReason = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'UNION\n'
+ 'SELECT CONCAT(idTestResult, \' - test result failure reason\')\n'
+ 'FROM TestResultFailures\n'
+ 'WHERE idFailureReason = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idFailureReason, idFailureReason,));
+ aaoRows = self._oDb.fetchAll();
+ if aaoRows:
+ raise TMRowInUse('Cannot remove failure reason %u because its being used by: %s'
+ % (idFailureReason, ', '.join(aoRow[0] for aoRow in aaoRows),));
+
+ #
+ # Do the job.
+ #
+ oData = FailureReasonData().initFromDbWithId(self._oDb, idFailureReason);
+ assert oData.idFailureReason == idFailureReason;
+ (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
+ if oData.tsEffective not in (tsCur, tsCurMinusOne):
+ self._historizeEntry(idFailureReason, tsCurMinusOne);
+ self._readdEntry(uidAuthor, oData, tsCurMinusOne);
+ self._historizeEntry(idFailureReason);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def cachedLookup(self, idFailureReason):
+ """
+ Looks up the most recent FailureReasonDataEx object for idFailureReason
+ via an object cache.
+
+ Returns a shared FailureReasonData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('FailureReasonDataEx');
+ oEntry = self.dCache.get(idFailureReason, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureReasons\n'
+ 'WHERE idFailureReason = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idFailureReason, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureReasons\n'
+ 'WHERE idFailureReason = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idFailureReason, ));
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason));
+
+ if self._oDb.getRowCount() == 1:
+ self._ensureCachesPresent();
+ oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
+ self.oUserAccountLogic);
+ self.dCache[idFailureReason] = oEntry;
+ return oEntry;
+
+
+ def cachedLookupByNameAndCategory(self, sName, sCategory):
+ """
+ Looks up a failure reason by it's name and category.
+
+ Should the request be ambigiuos, we'll return the oldest one.
+
+ Returns a shared FailureReasonData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCacheNameAndCat is None:
+ self.dCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category');
+ sKey = '%s:::%s' % (sName, sCategory,);
+ oEntry = self.dCacheNameAndCat.get(sKey, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM FailureReasons,\n'
+ ' FailureCategories\n'
+ 'WHERE FailureReasons.sShort = %s\n'
+ ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory '
+ ' AND FailureCategories.sShort = %s\n'
+ ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY FailureReasons.tsEffective\n'
+ , ( sName, sCategory));
+ if self._oDb.getRowCount() == 0:
+ sLikeSucks = self._oDb.formatBindArgs(
+ 'SELECT *\n'
+ 'FROM FailureReasons,\n'
+ ' FailureCategories\n'
+ 'WHERE ( FailureReasons.sShort ILIKE @@@@@@@! %s !@@@@@@@\n'
+ ' OR FailureReasons.sFull ILIKE @@@@@@@! %s !@@@@@@@)\n'
+ ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
+ ' AND ( FailureCategories.sShort = %s\n'
+ ' OR FailureCategories.sFull = %s)\n'
+ ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY FailureReasons.tsEffective\n'
+ , ( sName, sName, sCategory, sCategory ));
+ sLikeSucks = sLikeSucks.replace('LIKE @@@@@@@! \'', 'LIKE \'%').replace('\' !@@@@@@@', '%\'');
+ self._oDb.execute(sLikeSucks);
+ if self._oDb.getRowCount() > 0:
+ self._ensureCachesPresent();
+ oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
+ self.oUserAccountLogic);
+ self.dCacheNameAndCat[sKey] = oEntry;
+ if sName != oEntry.sShort or sCategory != oEntry.oCategory.sShort:
+ sKey2 = '%s:::%s' % (oEntry.sShort, oEntry.oCategory.sShort,);
+ self.dCacheNameAndCat[sKey2] = oEntry;
+ return oEntry;
+
+
+ #
+ # Helpers.
+ #
+
+ def _readdEntry(self, uidAuthor, oData, tsEffective = None):
+ """
+ Re-adds the FailureReasons entry. Used by addEntry, editEntry and removeEntry.
+ """
+ if tsEffective is None:
+ tsEffective = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('INSERT INTO FailureReasons (\n'
+ ' uidAuthor,\n'
+ ' tsEffective,\n'
+ ' idFailureReason,\n'
+ ' idFailureCategory,\n'
+ ' sShort,\n'
+ ' sFull,\n'
+ ' iTicket,\n'
+ ' asUrls)\n'
+ 'VALUES (%s, %s, '
+ + ( 'DEFAULT' if oData.idFailureReason is None else str(oData.idFailureReason) )
+ + ', %s, %s, %s, %s, %s)\n'
+ , ( uidAuthor,
+ tsEffective,
+ oData.idFailureCategory,
+ oData.sShort,
+ oData.sFull,
+ oData.iTicket,
+ oData.asUrls,) );
+ return True;
+
+
+ def _historizeEntry(self, idFailureReason, tsExpire = None):
+ """ Historizes the current entry. """
+ if tsExpire is None:
+ tsExpire = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('UPDATE FailureReasons\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idFailureReason = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (tsExpire, idFailureReason,));
+ return True;
+
+
+ def _ensureCachesPresent(self):
+ """ Ensures we've got the cache references resolved. """
+ if self.oCategoryLogic is None:
+ from testmanager.core.failurecategory import FailureCategoryLogic;
+ self.oCategoryLogic = FailureCategoryLogic(self._oDb);
+ if self.oUserAccountLogic is None:
+ self.oUserAccountLogic = UserAccountLogic(self._oDb);
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/globalresource.pgsql b/src/VBox/ValidationKit/testmanager/core/globalresource.pgsql
new file mode 100644
index 00000000..44b5c473
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/globalresource.pgsql
@@ -0,0 +1,118 @@
+-- $Id: globalresource.pgsql $
+--- @file
+-- VBox Test Manager Database Stored Procedures.
+--
+
+--
+-- Copyright (C) 2006-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+\set ON_ERROR_STOP 1
+\connect testmanager;
+
+-- Args: uidAuthor, sName, sDescription, fEnabled
+CREATE OR REPLACE function add_globalresource(integer, text, text, bool) RETURNS integer AS $$
+ DECLARE
+ _idGlobalRsrc integer;
+ _uidAuthor ALIAS FOR $1;
+ _sName ALIAS FOR $2;
+ _sDescription ALIAS FOR $3;
+ _fEnabled ALIAS FOR $4;
+ BEGIN
+ -- Check if Global Resource name is unique
+ IF EXISTS(SELECT * FROM GlobalResources
+ WHERE sName=_sName AND
+ tsExpire='infinity'::timestamp) THEN
+ RAISE EXCEPTION 'Duplicate Global Resource name';
+ END IF;
+ INSERT INTO GlobalResources (uidAuthor, sName, sDescription, fEnabled)
+ VALUES (_uidAuthor, _sName, _sDescription, _fEnabled) RETURNING idGlobalRsrc INTO _idGlobalRsrc;
+ RETURN _idGlobalRsrc;
+ END;
+$$ LANGUAGE plpgsql;
+
+-- Args: uidAuthor, idGlobalRsrc
+CREATE OR REPLACE function del_globalresource(integer, integer) RETURNS VOID AS $$
+ DECLARE
+ _uidAuthor ALIAS FOR $1;
+ _idGlobalRsrc ALIAS FOR $2;
+ BEGIN
+
+ -- Check if record exist
+ IF NOT EXISTS(SELECT * FROM GlobalResources WHERE idGlobalRsrc=_idGlobalRsrc AND tsExpire='infinity'::timestamp) THEN
+ RAISE EXCEPTION 'Global resource (%) does not exist', _idGlobalRsrc;
+ END IF;
+
+ -- Historize record: GlobalResources
+ UPDATE GlobalResources
+ SET tsExpire=CURRENT_TIMESTAMP,
+ uidAuthor=_uidAuthor
+ WHERE idGlobalRsrc=_idGlobalRsrc AND
+ tsExpire='infinity'::timestamp;
+
+
+ -- Delete record: GlobalResourceStatuses
+ DELETE FROM GlobalResourceStatuses WHERE idGlobalRsrc=_idGlobalRsrc;
+
+ -- Historize record: TestCaseGlobalRsrcDeps
+ UPDATE TestCaseGlobalRsrcDeps
+ SET tsExpire=CURRENT_TIMESTAMP,
+ uidAuthor=_uidAuthor
+ WHERE idGlobalRsrc=_idGlobalRsrc AND
+ tsExpire='infinity'::timestamp;
+
+ END;
+$$ LANGUAGE plpgsql;
+
+-- Args: uidAuthor, idGlobalRsrc, sName, sDescription, fEnabled
+CREATE OR REPLACE function update_globalresource(integer, integer, text, text, bool) RETURNS VOID AS $$
+ DECLARE
+ _uidAuthor ALIAS FOR $1;
+ _idGlobalRsrc ALIAS FOR $2;
+ _sName ALIAS FOR $3;
+ _sDescription ALIAS FOR $4;
+ _fEnabled ALIAS FOR $5;
+ BEGIN
+ -- Hostorize record
+ UPDATE GlobalResources
+ SET tsExpire=CURRENT_TIMESTAMP
+ WHERE idGlobalRsrc=_idGlobalRsrc AND
+ tsExpire='infinity'::timestamp;
+ -- Check if Global Resource name is unique
+ IF EXISTS(SELECT * FROM GlobalResources
+ WHERE sName=_sName AND
+ tsExpire='infinity'::timestamp) THEN
+ RAISE EXCEPTION 'Duplicate Global Resource name';
+ END IF;
+ -- Add new record
+ INSERT INTO GlobalResources(uidAuthor, idGlobalRsrc, sName, sDescription, fEnabled)
+ VALUES (_uidAuthor, _idGlobalRsrc, _sName, _sDescription, _fEnabled);
+ END;
+$$ LANGUAGE plpgsql;
diff --git a/src/VBox/ValidationKit/testmanager/core/globalresource.py b/src/VBox/ValidationKit/testmanager/core/globalresource.py
new file mode 100755
index 00000000..bd6f0e9e
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/globalresource.py
@@ -0,0 +1,328 @@
+# -*- coding: utf-8 -*-
+# $Id: globalresource.py $
+
+"""
+Test Manager - Global Resources.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMRowNotFound;
+
+
+class GlobalResourceData(ModelDataBase):
+ """
+ Global resource data
+ """
+
+ ksIdAttr = 'idGlobalRsrc';
+
+ ksParam_idGlobalRsrc = 'GlobalResource_idGlobalRsrc'
+ ksParam_tsEffective = 'GlobalResource_tsEffective'
+ ksParam_tsExpire = 'GlobalResource_tsExpire'
+ ksParam_uidAuthor = 'GlobalResource_uidAuthor'
+ ksParam_sName = 'GlobalResource_sName'
+ ksParam_sDescription = 'GlobalResource_sDescription'
+ ksParam_fEnabled = 'GlobalResource_fEnabled'
+
+ kasAllowNullAttributes = ['idGlobalRsrc', 'tsEffective', 'tsExpire', 'uidAuthor', 'sDescription' ];
+ kcchMin_sName = 2;
+ kcchMax_sName = 64;
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idGlobalRsrc = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.sName = None;
+ self.sDescription = None;
+ self.fEnabled = False
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM GlobalResources row.
+ Returns self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Global resource not found.')
+
+ self.idGlobalRsrc = aoRow[0]
+ self.tsEffective = aoRow[1]
+ self.tsExpire = aoRow[2]
+ self.uidAuthor = aoRow[3]
+ self.sName = aoRow[4]
+ self.sDescription = aoRow[5]
+ self.fEnabled = aoRow[6]
+ return self
+
+ def initFromDbWithId(self, oDb, idGlobalRsrc, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM GlobalResources\n'
+ 'WHERE idGlobalRsrc = %s\n'
+ , ( idGlobalRsrc,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idGlobalRsrc=%s not found (tsNow=%s sPeriodBack=%s)' % (idGlobalRsrc, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+ def isEqual(self, oOther):
+ """
+ Compares two instances.
+ """
+ return self.idGlobalRsrc == oOther.idGlobalRsrc \
+ and str(self.tsEffective) == str(oOther.tsEffective) \
+ and str(self.tsExpire) == str(oOther.tsExpire) \
+ and self.uidAuthor == oOther.uidAuthor \
+ and self.sName == oOther.sName \
+ and self.sDescription == oOther.sDescription \
+ and self.fEnabled == oOther.fEnabled
+
+
+class GlobalResourceLogic(ModelLogicBase):
+ """
+ Global resource logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Returns an array (list) of FailureReasonData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM GlobalResources\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY idGlobalRsrc DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM GlobalResources\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY idGlobalRsrc DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart,))
+
+ aoRows = []
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(GlobalResourceData().initFromDbRow(aoRow))
+ return aoRows
+
+
+ def cachedLookup(self, idGlobalRsrc):
+ """
+ Looks up the most recent GlobalResourceData object for idGlobalRsrc
+ via an object cache.
+
+ Returns a shared GlobalResourceData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('GlobalResourceData');
+ oEntry = self.dCache.get(idGlobalRsrc, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM GlobalResources\n'
+ 'WHERE idGlobalRsrc = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idGlobalRsrc, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM GlobalResources\n'
+ 'WHERE idGlobalRsrc = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idGlobalRsrc, ));
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idGlobalRsrc));
+
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ oEntry = GlobalResourceData();
+ oEntry.initFromDbRow(aaoRow);
+ self.dCache[idGlobalRsrc] = oEntry;
+ return oEntry;
+
+
+ def getAll(self, tsEffective = None):
+ """
+ Gets all global resources.
+
+ Returns an array of GlobalResourceData instances on success (can be
+ empty). Raises exception on database error.
+ """
+ if tsEffective is not None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM GlobalResources\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ , (tsEffective, tsEffective));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM GlobalResources\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n');
+ aaoRows = self._oDb.fetchAll();
+ aoRet = [];
+ for aoRow in aaoRows:
+ aoRet.append(GlobalResourceData().initFromDbRow(aoRow));
+
+ return aoRet;
+
+ def addGlobalResource(self, uidAuthor, oData):
+ """Add Global Resource DB record"""
+ self._oDb.execute('SELECT * FROM add_globalresource(%s, %s, %s, %s);',
+ (uidAuthor,
+ oData.sName,
+ oData.sDescription,
+ oData.fEnabled))
+ self._oDb.commit()
+ return True
+
+ def editGlobalResource(self, uidAuthor, idGlobalRsrc, oData):
+ """Modify Global Resource DB record"""
+ # Check if anything has been changed
+ oGlobalResourcesDataOld = self.getById(idGlobalRsrc)
+ if oGlobalResourcesDataOld.isEqual(oData):
+ # Nothing has been changed, do nothing
+ return True
+
+ self._oDb.execute('SELECT * FROM update_globalresource(%s, %s, %s, %s, %s);',
+ (uidAuthor,
+ idGlobalRsrc,
+ oData.sName,
+ oData.sDescription,
+ oData.fEnabled))
+ self._oDb.commit()
+ return True
+
+ def remove(self, uidAuthor, idGlobalRsrc):
+ """Delete Global Resource DB record"""
+ self._oDb.execute('SELECT * FROM del_globalresource(%s, %s);',
+ (uidAuthor, idGlobalRsrc))
+ self._oDb.commit()
+ return True
+
+ def getById(self, idGlobalRsrc):
+ """
+ Get global resource record by its id
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM GlobalResources\n'
+ 'WHERE tsExpire = \'infinity\'::timestamp\n'
+ ' AND idGlobalRsrc=%s;', (idGlobalRsrc,))
+
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise self._oDb.integrityException('Duplicate global resource entry with ID %u (current)' % (idGlobalRsrc,));
+ try:
+ return GlobalResourceData().initFromDbRow(aRows[0])
+ except IndexError:
+ raise TMRowNotFound('Global resource not found.')
+
+ def allocateResources(self, idTestBox, aoGlobalRsrcs, fCommit = False):
+ """
+ Allocates the given global resource.
+
+ Returns True of successfully allocated the resources, False if not.
+ May raise exception on DB error.
+ """
+ # Quit quickly if there is nothing to alloocate.
+ if not aoGlobalRsrcs:
+ return True;
+
+ #
+ # Note! Someone else might have allocated the resources since the
+ # scheduler check that they were available. In such case we
+ # need too quietly rollback and return FALSE.
+ #
+ self._oDb.execute('SAVEPOINT allocateResources');
+
+ for oGlobalRsrc in aoGlobalRsrcs:
+ try:
+ self._oDb.execute('INSERT INTO GlobalResourceStatuses (idGlobalRsrc, idTestBox)\n'
+ 'VALUES (%s, %s)', (oGlobalRsrc.idGlobalRsrc, idTestBox, ) );
+ except self._oDb.oXcptError:
+ self._oDb.execute('ROLLBACK TO SAVEPOINT allocateResources');
+ return False;
+
+ self._oDb.execute('RELEASE SAVEPOINT allocateResources');
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def freeGlobalResourcesByTestBox(self, idTestBox, fCommit = False):
+ """
+ Frees all global resources own by the given testbox.
+ Returns True. May raise exception on DB error.
+ """
+ self._oDb.execute('DELETE FROM GlobalResourceStatuses\n'
+ 'WHERE idTestBox = %s\n', (idTestBox, ) );
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class GlobalResourceDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [GlobalResourceData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/report.py b/src/VBox/ValidationKit/testmanager/core/report.py
new file mode 100755
index 00000000..f07e75e8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/report.py
@@ -0,0 +1,1307 @@
+# -*- coding: utf-8 -*-
+# $Id: report.py $
+
+"""
+Test Manager - Report models.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard Python imports.
+import sys;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelLogicBase, TMExceptionBase;
+from testmanager.core.build import BuildCategoryData;
+from testmanager.core.dbobjcache import DatabaseObjCache;
+from testmanager.core.failurereason import FailureReasonLogic;
+from testmanager.core.testbox import TestBoxLogic, TestBoxData;
+from testmanager.core.testcase import TestCaseLogic;
+from testmanager.core.testcaseargs import TestCaseArgsLogic;
+from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+from common import constants;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ xrange = range; # pylint: disable=redefined-builtin,invalid-name
+
+
+
+class ReportFilter(TestResultFilter):
+ """
+ Same as TestResultFilter for now.
+ """
+
+ def __init__(self):
+ TestResultFilter.__init__(self);
+
+
+
+class ReportModelBase(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Something all report logic(/miner) classes inherit from.
+ """
+
+ ## @name Report subjects
+ ## @{
+ ksSubEverything = 'Everything';
+ ksSubSchedGroup = 'SchedGroup';
+ ksSubTestGroup = 'TestGroup';
+ ksSubTestCase = 'TestCase';
+ ksSubTestCaseArgs = 'TestCaseArgs';
+ ksSubTestBox = 'TestBox';
+ ksSubBuild = 'Build';
+ ## @}
+ kasSubjects = [ ksSubEverything, ksSubSchedGroup, ksSubTestGroup, ksSubTestCase, ksSubTestBox, ksSubBuild, ];
+
+
+ ## @name TestStatus_T
+ # @{
+ ksTestStatus_Running = 'running';
+ ksTestStatus_Success = 'success';
+ ksTestStatus_Skipped = 'skipped';
+ ksTestStatus_BadTestBox = 'bad-testbox';
+ ksTestStatus_Aborted = 'aborted';
+ ksTestStatus_Failure = 'failure';
+ ksTestStatus_TimedOut = 'timed-out';
+ ksTestStatus_Rebooted = 'rebooted';
+ ## @}
+
+
+ def __init__(self, oDb, tsNow, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter):
+ ModelLogicBase.__init__(self, oDb);
+ # Public so the report generator can easily access them.
+ self.tsNow = tsNow; # (Can be None.)
+ self.__tsNowDateTime = None;
+ self.cPeriods = cPeriods;
+ self.cHoursPerPeriod = cHoursPerPeriod;
+ self.sSubject = sSubject;
+ self.aidSubjects = aidSubjects;
+ self.oFilter = oFilter;
+ if self.oFilter is None:
+ class DummyFilter(object):
+ """ Dummy """
+ def getTableJoins(self, sExtraIndent = '', iOmit = -1, dOmitTables = None):
+ """ Dummy """
+ _ = sExtraIndent; _ = iOmit; _ = dOmitTables; # pylint: disable=redefined-variable-type
+ return '';
+ def getWhereConditions(self, sExtraIndent = '', iOmit = -1):
+ """ Dummy """
+ _ = sExtraIndent; _ = iOmit; # pylint: disable=redefined-variable-type
+ return '';
+ def isJoiningWithTable(self, sTable):
+ """ Dummy """;
+ _ = sTable;
+ return False;
+ self.oFilter = DummyFilter();
+
+ def getExtraSubjectTables(self):
+ """
+ Returns a list of additional tables needed by the subject.
+ """
+ return [];
+
+ def getExtraSubjectWhereExpr(self):
+ """
+ Returns additional WHERE expression relating to the report subject. It starts
+ with an AND so that it can simply be appended to the WHERE clause.
+ """
+ if self.sSubject == self.ksSubEverything:
+ return '';
+
+ if self.sSubject == self.ksSubSchedGroup:
+ sWhere = ' AND TestSets.idSchedGroup';
+ elif self.sSubject == self.ksSubTestGroup:
+ sWhere = ' AND TestSets.idTestGroup';
+ elif self.sSubject == self.ksSubTestCase:
+ sWhere = ' AND TestSets.idTestCase';
+ elif self.sSubject == self.ksSubTestCaseArgs:
+ sWhere = ' AND TestSets.idTestCaseArgs';
+ elif self.sSubject == self.ksSubTestBox:
+ sWhere = ' AND TestSets.idTestBox';
+ elif self.sSubject == self.ksSubBuild:
+ sWhere = ' AND TestSets.idBuild';
+ else:
+ raise TMExceptionBase(self.sSubject);
+
+ if len(self.aidSubjects) == 1:
+ sWhere += self._oDb.formatBindArgs(' = %s\n', (self.aidSubjects[0],));
+ else:
+ assert self.aidSubjects;
+ sWhere += self._oDb.formatBindArgs(' IN (%s', (self.aidSubjects[0],));
+ for i in range(1, len(self.aidSubjects)):
+ sWhere += self._oDb.formatBindArgs(', %s', (self.aidSubjects[i],));
+ sWhere += ')\n';
+
+ return sWhere;
+
+ def getNowAsDateTime(self):
+ """ Returns a datetime instance corresponding to tsNow. """
+ if self.__tsNowDateTime is None:
+ if self.tsNow is None:
+ self.__tsNowDateTime = self._oDb.getCurrentTimestamp();
+ else:
+ self._oDb.execute('SELECT %s::TIMESTAMP WITH TIME ZONE', (self.tsNow,));
+ self.__tsNowDateTime = self._oDb.fetchOne()[0];
+ return self.__tsNowDateTime;
+
+ def getPeriodStart(self, iPeriod):
+ """ Gets the python timestamp for the start of the given period. """
+ from datetime import timedelta;
+ cHoursStart = (self.cPeriods - iPeriod ) * self.cHoursPerPeriod;
+ return self.getNowAsDateTime() - timedelta(hours = cHoursStart);
+
+ def getPeriodEnd(self, iPeriod):
+ """ Gets the python timestamp for the end of the given period. """
+ from datetime import timedelta;
+ cHoursEnd = (self.cPeriods - iPeriod - 1) * self.cHoursPerPeriod;
+ return self.getNowAsDateTime() - timedelta(hours = cHoursEnd);
+
+ def getExtraWhereExprForPeriod(self, iPeriod):
+ """
+ Returns additional WHERE expression for getting test sets for the
+ specified period. It starts with an AND so that it can simply be
+ appended to the WHERE clause.
+ """
+ if self.tsNow is None:
+ sNow = 'CURRENT_TIMESTAMP';
+ else:
+ sNow = self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,));
+
+ cHoursStart = (self.cPeriods - iPeriod ) * self.cHoursPerPeriod;
+ cHoursEnd = (self.cPeriods - iPeriod - 1) * self.cHoursPerPeriod;
+ if cHoursEnd == 0:
+ return ' AND TestSets.tsDone >= (%s - interval \'%u hours\')\n' \
+ ' AND TestSets.tsDone < %s\n' \
+ % (sNow, cHoursStart, sNow);
+ return ' AND TestSets.tsDone >= (%s - interval \'%u hours\')\n' \
+ ' AND TestSets.tsDone < (%s - interval \'%u hours\')\n' \
+ % (sNow, cHoursStart, sNow, cHoursEnd);
+
+ def getPeriodDesc(self, iPeriod):
+ """
+ Returns the period description, usually for graph data.
+ """
+ if iPeriod == 0:
+ return 'now' if self.tsNow is None else 'then';
+ sTerm = 'ago' if self.tsNow is None else 'earlier';
+ if self.cHoursPerPeriod == 24:
+ return '%dd %s' % (iPeriod, sTerm, );
+ if (iPeriod * self.cHoursPerPeriod) % 24 == 0:
+ return '%dd %s' % (iPeriod * self.cHoursPerPeriod / 24, sTerm, );
+ return '%dh %s' % (iPeriod * self.cHoursPerPeriod, sTerm);
+
+ def getStraightPeriodDesc(self, iPeriod):
+ """
+ Returns the period description, usually for graph data.
+ """
+ iWickedPeriod = self.cPeriods - iPeriod - 1;
+ return self.getPeriodDesc(iWickedPeriod);
+
+
+#
+# Data structures produced and returned by the ReportLazyModel.
+#
+
+class ReportTransientBase(object):
+ """ Details on the test where a problem was first/last seen. """
+ def __init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, # pylint: disable=too-many-arguments
+ iPeriod, fEnter, idSubject, oSubject):
+ self.idBuild = idBuild; # Build ID.
+ self.iRevision = iRevision; # SVN revision for build.
+ self.sRepository = sRepository; # SVN repository for build.
+ self.idTestSet = idTestSet; # Test set.
+ self.idTestResult = idTestResult; # Test result.
+ self.tsDone = tsDone; # When the test set was done.
+ self.iPeriod = iPeriod; # Data set period.
+ self.fEnter = fEnter; # True if enter event, False if leave event.
+ self.idSubject = idSubject;
+ self.oSubject = oSubject;
+
+class ReportFailureReasonTransient(ReportTransientBase):
+ """ Details on the test where a failure reason was first/last seen. """
+ def __init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, # pylint: disable=too-many-arguments
+ iPeriod, fEnter, oReason):
+ ReportTransientBase.__init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, iPeriod, fEnter,
+ oReason.idFailureReason, oReason);
+ self.oReason = oReason; # FailureReasonDataEx
+
+
+class ReportHitRowBase(object):
+ """ A row in a period. """
+ def __init__(self, idSubject, oSubject, cHits, tsMin = None, tsMax = None):
+ self.idSubject = idSubject;
+ self.oSubject = oSubject;
+ self.cHits = cHits;
+ self.tsMin = tsMin;
+ self.tsMax = tsMax;
+
+class ReportHitRowWithTotalBase(ReportHitRowBase):
+ """ A row in a period. """
+ def __init__(self, idSubject, oSubject, cHits, cTotal, tsMin = None, tsMax = None):
+ ReportHitRowBase.__init__(self, idSubject, oSubject, cHits, tsMin, tsMax)
+ self.cTotal = cTotal;
+ self.uPct = cHits * 100 / cTotal;
+
+class ReportFailureReasonRow(ReportHitRowBase):
+ """ The account of one failure reason for a period. """
+ def __init__(self, aoRow, oReason):
+ ReportHitRowBase.__init__(self, aoRow[0], oReason, aoRow[1], aoRow[2], aoRow[3]);
+ self.idFailureReason = aoRow[0];
+ self.oReason = oReason; # FailureReasonDataEx
+
+
+class ReportPeriodBase(object):
+ """ A period in ReportFailureReasonSet. """
+ def __init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo):
+ self.oSet = oSet # Reference to the parent ReportSetBase derived object.
+ self.iPeriod = iPeriod; # Period number in the set.
+ self.sDesc = sDesc; # Short period description.
+ self.tsStart = tsFrom; # Start of the period.
+ self.tsEnd = tsTo; # End of the period (exclusive).
+ self.tsMin = tsTo; # The earlierst hit of the period (only valid for cHits > 0).
+ self.tsMax = tsFrom; # The latest hit of the period (only valid for cHits > 0).
+ self.aoRows = []; # Rows in order the database returned them (ReportHitRowBase descendant).
+ self.dRowsById = {}; # Same as aoRows but indexed by object ID (see ReportSetBase::sIdAttr).
+ self.dFirst = {}; # The subjects seen for the first time - data object, keyed by ID.
+ self.dLast = {}; # The subjects seen for the last time - data object, keyed by ID.
+ self.cHits = 0; # Total number of hits in this period.
+ self.cMaxHits = 0; # Max hits in a row.
+ self.cMinHits = 99999999; # Min hits in a row (only valid for cHits > 0).
+
+ def appendRow(self, oRow, idRow, oData):
+ """ Adds a row. """
+ assert isinstance(oRow, ReportHitRowBase);
+ self.aoRows.append(oRow);
+ self.dRowsById[idRow] = oRow;
+ if idRow not in self.oSet.dSubjects:
+ self.oSet.dSubjects[idRow] = oData;
+ self._doStatsForRow(oRow, idRow, oData);
+
+ def _doStatsForRow(self, oRow, idRow, oData):
+ """ Does the statistics for a row. Helper for appendRow as well as helpRecalcStats. """
+ if oRow.tsMin is not None and oRow.tsMin < self.tsMin:
+ self.tsMin = oRow.tsMin;
+ if oRow.tsMax is not None and oRow.tsMax < self.tsMax:
+ self.tsMax = oRow.tsMax;
+
+ self.cHits += oRow.cHits;
+ if oRow.cHits > self.cMaxHits:
+ self.cMaxHits = oRow.cHits;
+ if oRow.cHits < self.cMinHits:
+ self.cMinHits = oRow.cHits;
+
+ if idRow in self.oSet.dcHitsPerId:
+ self.oSet.dcHitsPerId[idRow] += oRow.cHits;
+ else:
+ self.oSet.dcHitsPerId[idRow] = oRow.cHits;
+
+ if oRow.cHits > 0:
+ if idRow not in self.oSet.diPeriodFirst:
+ self.dFirst[idRow] = oData;
+ self.oSet.diPeriodFirst[idRow] = self.iPeriod;
+ self.oSet.diPeriodLast[idRow] = self.iPeriod;
+
+ def helperSetRecalcStats(self):
+ """ Recalc the statistics (do resetStats first on set). """
+ for idRow, oRow in self.dRowsById.items():
+ self._doStatsForRow(oRow, idRow, self.oSet.dSubjects[idRow]);
+
+ def helperSetResetStats(self):
+ """ Resets the statistics. """
+ self.tsMin = self.tsEnd;
+ self.tsMax = self.tsStart;
+ self.cHits = 0;
+ self.cMaxHits = 0;
+ self.cMinHits = 99999999;
+ self.dFirst = {};
+ self.dLast = {};
+
+ def helperSetDeleteKeyFromSet(self, idKey):
+ """ Helper for ReportPeriodSetBase::deleteKey """
+ if idKey in self.dRowsById:
+ oRow = self.dRowsById[idKey];
+ self.aoRows.remove(oRow);
+ del self.dRowsById[idKey]
+ self.cHits -= oRow.cHits;
+ if idKey in self.dFirst:
+ del self.dFirst[idKey];
+ if idKey in self.dLast:
+ del self.dLast[idKey];
+
+class ReportPeriodWithTotalBase(ReportPeriodBase):
+ """ In addition to the cHits, we also have a total to relate it too. """
+ def __init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo):
+ ReportPeriodBase.__init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo);
+ self.cTotal = 0;
+ self.cMaxTotal = 0;
+ self.cMinTotal = 99999999;
+ self.uMaxPct = 0; # Max percentage in a row (100 = 100%).
+
+ def _doStatsForRow(self, oRow, idRow, oData):
+ assert isinstance(oRow, ReportHitRowWithTotalBase);
+ super(ReportPeriodWithTotalBase, self)._doStatsForRow(oRow, idRow, oData);
+ self.cTotal += oRow.cTotal;
+ if oRow.cTotal > self.cMaxTotal:
+ self.cMaxTotal = oRow.cTotal;
+ if oRow.cTotal < self.cMinTotal:
+ self.cMinTotal = oRow.cTotal;
+
+ if oRow.uPct > self.uMaxPct:
+ self.uMaxPct = oRow.uPct;
+
+ if idRow in self.oSet.dcTotalPerId:
+ self.oSet.dcTotalPerId[idRow] += oRow.cTotal;
+ else:
+ self.oSet.dcTotalPerId[idRow] = oRow.cTotal;
+
+ def helperSetResetStats(self):
+ super(ReportPeriodWithTotalBase, self).helperSetResetStats();
+ self.cTotal = 0;
+ self.cMaxTotal = 0;
+ self.cMinTotal = 99999999;
+ self.uMaxPct = 0;
+
+class ReportFailureReasonPeriod(ReportPeriodBase):
+ """ A period in ReportFailureReasonSet. """
+ def __init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo):
+ ReportPeriodBase.__init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo);
+ self.cWithoutReason = 0; # Number of failed test sets without any assigned reason.
+
+
+
+class ReportPeriodSetBase(object):
+ """ Period data set base class. """
+ def __init__(self, sIdAttr):
+ self.sIdAttr = sIdAttr; # The name of the key attribute. Mainly for documentation purposes.
+ self.aoPeriods = []; # Periods (ReportPeriodBase descendant) in ascending order (time wise).
+ self.dSubjects = {}; # The subject data objects, keyed by the subject ID.
+ self.dcHitsPerId = {}; # Sum hits per subject ID (key).
+ self.cHits = 0; # Sum number of hits in all periods and all reasons.
+ self.cMaxHits = 0; # Max hits in a row.
+ self.cMinHits = 99999999; # Min hits in a row.
+ self.cMaxRows = 0; # Max number of rows in a period.
+ self.cMinRows = 99999999; # Min number of rows in a period.
+ self.diPeriodFirst = {}; # The period number a reason was first seen (keyed by subject ID).
+ self.diPeriodLast = {}; # The period number a reason was last seen (keyed by subject ID).
+ self.aoEnterInfo = []; # Array of ReportTransientBase children order by iRevision. Excludes
+ # the first period of course. (Child class populates this.)
+ self.aoLeaveInfo = []; # Array of ReportTransientBase children order in descending order by
+ # iRevision. Excludes the last priod. (Child class populates this.)
+
+ def appendPeriod(self, oPeriod):
+ """ Appends a period to the set. """
+ assert isinstance(oPeriod, ReportPeriodBase);
+ self.aoPeriods.append(oPeriod);
+ self._doStatsForPeriod(oPeriod);
+
+ def _doStatsForPeriod(self, oPeriod):
+ """ Worker for appendPeriod and recalcStats. """
+ self.cHits += oPeriod.cHits;
+ if oPeriod.cMaxHits > self.cMaxHits:
+ self.cMaxHits = oPeriod.cMaxHits;
+ if oPeriod.cMinHits < self.cMinHits:
+ self.cMinHits = oPeriod.cMinHits;
+
+ if len(oPeriod.aoRows) > self.cMaxRows:
+ self.cMaxRows = len(oPeriod.aoRows);
+ if len(oPeriod.aoRows) < self.cMinRows:
+ self.cMinRows = len(oPeriod.aoRows);
+
+ def recalcStats(self):
+ """ Recalculates the statistics. ASSUMES finalizePass1 hasn't been done yet. """
+ self.cHits = 0;
+ self.cMaxHits = 0;
+ self.cMinHits = 99999999;
+ self.cMaxRows = 0;
+ self.cMinRows = 99999999;
+ self.diPeriodFirst = {};
+ self.diPeriodLast = {};
+ self.dcHitsPerId = {};
+ for oPeriod in self.aoPeriods:
+ oPeriod.helperSetResetStats();
+
+ for oPeriod in self.aoPeriods:
+ oPeriod.helperSetRecalcStats();
+ self._doStatsForPeriod(oPeriod);
+
+ def deleteKey(self, idKey):
+ """ Deletes a key from the set. May leave cMaxHits and cMinHits with outdated values. """
+ self.cHits -= self.dcHitsPerId[idKey];
+ del self.dcHitsPerId[idKey];
+ if idKey in self.diPeriodFirst:
+ del self.diPeriodFirst[idKey];
+ if idKey in self.diPeriodLast:
+ del self.diPeriodLast[idKey];
+ if idKey in self.aoEnterInfo:
+ del self.aoEnterInfo[idKey];
+ if idKey in self.aoLeaveInfo:
+ del self.aoLeaveInfo[idKey];
+ del self.dSubjects[idKey];
+ for oPeriod in self.aoPeriods:
+ oPeriod.helperSetDeleteKeyFromSet(idKey);
+
+ def pruneRowsWithZeroSumHits(self):
+ """ Discards rows with zero sum hits across all periods. Works around lazy selects counting both totals and hits. """
+ cDeleted = 0;
+ aidKeys = list(self.dcHitsPerId);
+ for idKey in aidKeys:
+ if self.dcHitsPerId[idKey] == 0:
+ self.deleteKey(idKey);
+ cDeleted += 1;
+ if cDeleted > 0:
+ self.recalcStats();
+ return cDeleted;
+
+ def finalizePass1(self):
+ """ Finished all but aoEnterInfo and aoLeaveInfo. """
+ # All we need to do here is to populate the dLast members.
+ for idKey, iPeriod in self.diPeriodLast.items():
+ self.aoPeriods[iPeriod].dLast[idKey] = self.dSubjects[idKey];
+ return self;
+
+ def finalizePass2(self):
+ """ Called after aoEnterInfo and aoLeaveInfo has been populated to sort them. """
+ self.aoEnterInfo = sorted(self.aoEnterInfo, key = lambda oTrans: oTrans.iRevision);
+ self.aoLeaveInfo = sorted(self.aoLeaveInfo, key = lambda oTrans: oTrans.iRevision, reverse = True);
+ return self;
+
+class ReportPeriodSetWithTotalBase(ReportPeriodSetBase):
+ """ In addition to the cHits, we also have a total to relate it too. """
+ def __init__(self, sIdAttr):
+ ReportPeriodSetBase.__init__(self, sIdAttr);
+ self.dcTotalPerId = {}; # Sum total per subject ID (key).
+ self.cTotal = 0; # Sum number of total in all periods and all reasons.
+ self.cMaxTotal = 0; # Max total in a row.
+ self.cMinTotal = 0; # Min total in a row.
+ self.uMaxPct = 0; # Max percentage in a row (100 = 100%).
+
+ def _doStatsForPeriod(self, oPeriod):
+ assert isinstance(oPeriod, ReportPeriodWithTotalBase);
+ super(ReportPeriodSetWithTotalBase, self)._doStatsForPeriod(oPeriod);
+ self.cTotal += oPeriod.cTotal;
+ if oPeriod.cMaxTotal > self.cMaxTotal:
+ self.cMaxTotal = oPeriod.cMaxTotal;
+ if oPeriod.cMinTotal < self.cMinTotal:
+ self.cMinTotal = oPeriod.cMinTotal;
+
+ if oPeriod.uMaxPct > self.uMaxPct:
+ self.uMaxPct = oPeriod.uMaxPct;
+
+ def recalcStats(self):
+ self.dcTotalPerId = {};
+ self.cTotal = 0;
+ self.cMaxTotal = 0;
+ self.cMinTotal = 0;
+ self.uMaxPct = 0;
+ super(ReportPeriodSetWithTotalBase, self).recalcStats();
+
+ def deleteKey(self, idKey):
+ self.cTotal -= self.dcTotalPerId[idKey];
+ del self.dcTotalPerId[idKey];
+ super(ReportPeriodSetWithTotalBase, self).deleteKey(idKey);
+
+class ReportFailureReasonSet(ReportPeriodSetBase):
+ """ What ReportLazyModel.getFailureReasons returns. """
+ def __init__(self):
+ ReportPeriodSetBase.__init__(self, 'idFailureReason');
+
+
+
+class ReportLazyModel(ReportModelBase): # pylint: disable=too-few-public-methods
+ """
+ The 'lazy bird' report model class.
+
+ We may want to have several classes, maybe one for each report even. But,
+ I'm thinking that's a bit overkill so we'll start with this and split it
+ if/when it becomes necessary.
+ """
+
+ kdsStatusSimplificationMap = {
+ ReportModelBase.ksTestStatus_Running: ReportModelBase.ksTestStatus_Running,
+ ReportModelBase.ksTestStatus_Success: ReportModelBase.ksTestStatus_Success,
+ ReportModelBase.ksTestStatus_Skipped: ReportModelBase.ksTestStatus_Skipped,
+ ReportModelBase.ksTestStatus_BadTestBox: ReportModelBase.ksTestStatus_Skipped,
+ ReportModelBase.ksTestStatus_Aborted: ReportModelBase.ksTestStatus_Skipped,
+ ReportModelBase.ksTestStatus_Failure: ReportModelBase.ksTestStatus_Failure,
+ ReportModelBase.ksTestStatus_TimedOut: ReportModelBase.ksTestStatus_Failure,
+ ReportModelBase.ksTestStatus_Rebooted: ReportModelBase.ksTestStatus_Failure,
+ };
+
+ def getSuccessRates(self):
+ """
+ Gets the success rates of the subject in the specified period.
+
+ Returns an array of data per period (0 is the oldes, self.cPeriods-1 is
+ the latest) where each entry is a status (TestStatus_T) dictionary with
+ the number of occurences of each final status (i.e. not running).
+ """
+
+ sBaseQuery = 'SELECT TestSets.enmStatus, COUNT(TestSets.idTestSet)\n' \
+ 'FROM TestSets\n' \
+ + self.oFilter.getTableJoins();
+ for sTable in self.getExtraSubjectTables():
+ sBaseQuery = sBaseQuery[:-1] + ',\n ' + sTable + '\n';
+ sBaseQuery += 'WHERE enmStatus <> \'running\'\n' \
+ + self.oFilter.getWhereConditions() \
+ + self.getExtraSubjectWhereExpr();
+
+ adPeriods = [];
+ for iPeriod in xrange(self.cPeriods):
+ self._oDb.execute(sBaseQuery + self.getExtraWhereExprForPeriod(iPeriod) + 'GROUP BY enmStatus\n');
+
+ dRet = \
+ {
+ self.ksTestStatus_Skipped: 0,
+ self.ksTestStatus_Failure: 0,
+ self.ksTestStatus_Success: 0,
+ };
+
+ for aoRow in self._oDb.fetchAll():
+ sKey = self.kdsStatusSimplificationMap[aoRow[0]]
+ if sKey in dRet:
+ dRet[sKey] += aoRow[1];
+ else:
+ dRet[sKey] = aoRow[1];
+
+ assert len(dRet) == 3;
+
+ adPeriods.insert(0, dRet);
+
+ return adPeriods;
+
+
+ def getFailureReasons(self):
+ """
+ Gets the failure reasons of the subject in the specified period.
+
+ Returns a ReportFailureReasonSet instance.
+ """
+
+ oFailureReasonLogic = FailureReasonLogic(self._oDb);
+
+ #
+ # Create a temporary table
+ #
+ sTsNow = 'CURRENT_TIMESTAMP' if self.tsNow is None else self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,));
+ sTsFirst = '(%s - interval \'%s hours\')' \
+ % (sTsNow, self.cHoursPerPeriod * self.cPeriods,);
+ sQuery = 'CREATE TEMPORARY TABLE TmpReasons ON COMMIT DROP AS\n' \
+ 'SELECT TestResultFailures.idFailureReason AS idFailureReason,\n' \
+ ' TestResultFailures.idTestResult AS idTestResult,\n' \
+ ' TestSets.idTestSet AS idTestSet,\n' \
+ ' TestSets.tsDone AS tsDone,\n' \
+ ' TestSets.tsCreated AS tsCreated,\n' \
+ ' TestSets.idBuild AS idBuild\n' \
+ 'FROM TestResultFailures,\n' \
+ ' TestResults,\n' \
+ ' TestSets\n' \
+ + self.oFilter.getTableJoins(dOmitTables = {'TestResults': True, 'TestResultFailures': True});
+ for sTable in self.getExtraSubjectTables():
+ if sTable not in [ 'TestResults', 'TestResultFailures' ] and not self.oFilter.isJoiningWithTable(sTable):
+ sQuery = sQuery[:-1] + ',\n ' + sTable + '\n';
+ sQuery += 'WHERE TestResultFailures.idTestResult = TestResults.idTestResult\n' \
+ ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' \
+ ' AND TestResultFailures.tsEffective >= ' + sTsFirst + '\n' \
+ ' AND TestResults.enmStatus <> \'running\'\n' \
+ ' AND TestResults.enmStatus <> \'success\'\n' \
+ ' AND TestResults.tsCreated >= ' + sTsFirst + '\n' \
+ ' AND TestResults.tsCreated < ' + sTsNow + '\n' \
+ ' AND TestResults.idTestSet = TestSets.idTestSet\n' \
+ ' AND TestSets.tsDone >= ' + sTsFirst + '\n' \
+ ' AND TestSets.tsDone < ' + sTsNow + '\n' \
+ + self.oFilter.getWhereConditions() \
+ + self.getExtraSubjectWhereExpr();
+ self._oDb.execute(sQuery);
+ self._oDb.execute('SELECT idFailureReason FROM TmpReasons;');
+
+ #
+ # Retrieve the period results.
+ #
+ oSet = ReportFailureReasonSet();
+ for iPeriod in xrange(self.cPeriods):
+ self._oDb.execute('SELECT idFailureReason,\n'
+ ' COUNT(idTestResult),\n'
+ ' MIN(tsDone),\n'
+ ' MAX(tsDone)\n'
+ 'FROM TmpReasons\n'
+ 'WHERE TRUE\n'
+ + self.getExtraWhereExprForPeriod(iPeriod).replace('TestSets.', '') +
+ 'GROUP BY idFailureReason\n');
+ aaoRows = self._oDb.fetchAll()
+
+ oPeriod = ReportFailureReasonPeriod(oSet, iPeriod, self.getStraightPeriodDesc(iPeriod),
+ self.getPeriodStart(iPeriod), self.getPeriodEnd(iPeriod));
+
+ for aoRow in aaoRows:
+ oReason = oFailureReasonLogic.cachedLookup(aoRow[0]);
+ oPeriodRow = ReportFailureReasonRow(aoRow, oReason);
+ oPeriod.appendRow(oPeriodRow, oReason.idFailureReason, oReason);
+
+ # Count how many test sets we've got without any reason associated with them.
+ self._oDb.execute('SELECT COUNT(TestSets.idTestSet)\n'
+ 'FROM TestSets\n'
+ ' LEFT OUTER JOIN TestResultFailures\n'
+ ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n'
+ ' AND TestResultFailures.tsEffective = \'infinity\'::TIMESTAMP\n'
+ 'WHERE TestSets.enmStatus <> \'running\'\n'
+ ' AND TestSets.enmStatus <> \'success\'\n'
+ + self.getExtraWhereExprForPeriod(iPeriod) +
+ ' AND TestResultFailures.idTestSet IS NULL\n');
+ oPeriod.cWithoutReason = self._oDb.fetchOne()[0];
+
+ oSet.appendPeriod(oPeriod);
+
+
+ #
+ # For reasons entering after the first period, look up the build and
+ # test set it first occured with.
+ #
+ oSet.finalizePass1();
+
+ for iPeriod in xrange(1, self.cPeriods):
+ oPeriod = oSet.aoPeriods[iPeriod];
+ for oReason in oPeriod.dFirst.values():
+ oSet.aoEnterInfo.append(self._getEdgeFailureReasonOccurence(oReason, iPeriod, fEnter = True));
+
+ # Ditto for reasons leaving before the last.
+ for iPeriod in xrange(self.cPeriods - 1):
+ oPeriod = oSet.aoPeriods[iPeriod];
+ for oReason in oPeriod.dLast.values():
+ oSet.aoLeaveInfo.append(self._getEdgeFailureReasonOccurence(oReason, iPeriod, fEnter = False));
+
+ oSet.finalizePass2();
+
+ self._oDb.execute('DROP TABLE TmpReasons\n');
+ return oSet;
+
+
+ def _getEdgeFailureReasonOccurence(self, oReason, iPeriod, fEnter = True):
+ """
+ Helper for the failure reason report that finds the oldest or newest build
+ (SVN rev) and test set (start time) it occured with.
+
+ If fEnter is set the oldest occurence is return, if fEnter clear the newest
+ is is returned.
+
+ Returns ReportFailureReasonTransient instant.
+
+ """
+
+
+ sSorting = 'ASC' if fEnter else 'DESC';
+ self._oDb.execute('SELECT TmpReasons.idTestResult,\n'
+ ' TmpReasons.idTestSet,\n'
+ ' TmpReasons.tsDone,\n'
+ ' TmpReasons.idBuild,\n'
+ ' Builds.iRevision,\n'
+ ' BuildCategories.sRepository\n'
+ 'FROM TmpReasons,\n'
+ ' Builds,\n'
+ ' BuildCategories\n'
+ 'WHERE TmpReasons.idFailureReason = %s\n'
+ ' AND TmpReasons.idBuild = Builds.idBuild\n'
+ ' AND Builds.tsExpire > TmpReasons.tsCreated\n'
+ ' AND Builds.tsEffective <= TmpReasons.tsCreated\n'
+ ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ 'ORDER BY Builds.iRevision ' + sSorting + ',\n'
+ ' TmpReasons.tsCreated ' + sSorting + '\n'
+ 'LIMIT 1\n'
+ , ( oReason.idFailureReason, ));
+ aoRow = self._oDb.fetchOne();
+ if aoRow is None:
+ return ReportFailureReasonTransient(-1, -1, 'internal-error', -1, -1, self._oDb.getCurrentTimestamp(),
+ iPeriod, fEnter, oReason);
+ return ReportFailureReasonTransient(idBuild = aoRow[3], iRevision = aoRow[4], sRepository = aoRow[5],
+ idTestSet = aoRow[1], idTestResult = aoRow[0], tsDone = aoRow[2],
+ iPeriod = iPeriod, fEnter = fEnter, oReason = oReason);
+
+
+ def getTestCaseFailures(self):
+ """
+ Gets the test case failures of the subject in the specified period.
+
+ Returns a ReportPeriodSetWithTotalBase instance.
+
+ """
+ return self._getSimpleFailures('idTestCase', TestCaseLogic);
+
+
+ def getTestCaseVariationFailures(self):
+ """
+ Gets the test case failures of the subject in the specified period.
+
+ Returns a ReportPeriodSetWithTotalBase instance.
+
+ """
+ return self._getSimpleFailures('idTestCaseArgs', TestCaseArgsLogic);
+
+
+ def getTestBoxFailures(self):
+ """
+ Gets the test box failures of the subject in the specified period.
+
+ Returns a ReportPeriodSetWithTotalBase instance.
+
+ """
+ return self._getSimpleFailures('idTestBox', TestBoxLogic);
+
+
+ def _getSimpleFailures(self, sIdColumn, oCacheLogicType, sIdAttr = None):
+ """
+ Gets the test box failures of the subject in the specified period.
+
+ Returns a ReportPeriodSetWithTotalBase instance.
+
+ """
+
+ oLogic = oCacheLogicType(self._oDb);
+ oSet = ReportPeriodSetWithTotalBase(sIdColumn if sIdAttr is None else sIdAttr);
+
+ # Construct base query.
+ sBaseQuery = 'SELECT TestSets.' + sIdColumn + ',\n' \
+ ' COUNT(CASE WHEN TestSets.enmStatus >= \'failure\' THEN 1 END),\n' \
+ ' MIN(TestSets.tsDone),\n' \
+ ' MAX(TestSets.tsDone),\n' \
+ ' COUNT(TestSets.idTestResult)\n' \
+ 'FROM TestSets\n' \
+ + self.oFilter.getTableJoins();
+ for sTable in self.getExtraSubjectTables():
+ sBaseQuery = sBaseQuery[:-1] + ',\n ' + sTable + '\n';
+ sBaseQuery += 'WHERE TRUE\n' \
+ + self.oFilter.getWhereConditions() \
+ + self.getExtraSubjectWhereExpr() + '\n';
+
+ # Retrieve the period results.
+ for iPeriod in xrange(self.cPeriods):
+ self._oDb.execute(sBaseQuery + self.getExtraWhereExprForPeriod(iPeriod) + 'GROUP BY TestSets.' + sIdColumn + '\n');
+ aaoRows = self._oDb.fetchAll()
+
+ oPeriod = ReportPeriodWithTotalBase(oSet, iPeriod, self.getStraightPeriodDesc(iPeriod),
+ self.getPeriodStart(iPeriod), self.getPeriodEnd(iPeriod));
+
+ for aoRow in aaoRows:
+ oSubject = oLogic.cachedLookup(aoRow[0]);
+ oPeriodRow = ReportHitRowWithTotalBase(aoRow[0], oSubject, aoRow[1], aoRow[4], aoRow[2], aoRow[3]);
+ oPeriod.appendRow(oPeriodRow, aoRow[0], oSubject);
+
+ oSet.appendPeriod(oPeriod);
+ oSet.pruneRowsWithZeroSumHits();
+
+
+
+ #
+ # For reasons entering after the first period, look up the build and
+ # test set it first occured with.
+ #
+ oSet.finalizePass1();
+
+ for iPeriod in xrange(1, self.cPeriods):
+ oPeriod = oSet.aoPeriods[iPeriod];
+ for idSubject, oSubject in oPeriod.dFirst.items():
+ oSet.aoEnterInfo.append(self._getEdgeSimpleFailureOccurence(idSubject, sIdColumn, oSubject,
+ iPeriod, fEnter = True));
+
+ # Ditto for reasons leaving before the last.
+ for iPeriod in xrange(self.cPeriods - 1):
+ oPeriod = oSet.aoPeriods[iPeriod];
+ for idSubject, oSubject in oPeriod.dLast.items():
+ oSet.aoLeaveInfo.append(self._getEdgeSimpleFailureOccurence(idSubject, sIdColumn, oSubject,
+ iPeriod, fEnter = False));
+
+ oSet.finalizePass2();
+
+ return oSet;
+
+ def _getEdgeSimpleFailureOccurence(self, idSubject, sIdColumn, oSubject, iPeriod, fEnter = True):
+ """
+ Helper for the failure reason report that finds the oldest or newest build
+ (SVN rev) and test set (start time) it occured with.
+
+ If fEnter is set the oldest occurence is return, if fEnter clear the newest
+ is is returned.
+
+ Returns ReportTransientBase instant.
+
+ """
+ sSorting = 'ASC' if fEnter else 'DESC';
+ sQuery = 'SELECT TestSets.idTestResult,\n' \
+ ' TestSets.idTestSet,\n' \
+ ' TestSets.tsDone,\n' \
+ ' TestSets.idBuild,\n' \
+ ' Builds.iRevision,\n' \
+ ' BuildCategories.sRepository\n' \
+ 'FROM TestSets\n' \
+ + self.oFilter.getTableJoins(dOmitTables = {'Builds': True, 'BuildCategories': True});
+ sQuery = sQuery[:-1] + ',\n' \
+ ' Builds,\n' \
+ ' BuildCategories\n';
+ for sTable in self.getExtraSubjectTables():
+ if sTable not in [ 'Builds', 'BuildCategories' ] and not self.oFilter.isJoiningWithTable(sTable):
+ sQuery = sQuery[:-1] + ',\n ' + sTable + '\n';
+ sQuery += 'WHERE TestSets.' + sIdColumn + ' = ' + str(idSubject) + '\n' \
+ ' AND TestSets.idBuild = Builds.idBuild\n' \
+ ' AND TestSets.enmStatus >= \'failure\'\n' \
+ + self.getExtraWhereExprForPeriod(iPeriod) + \
+ ' AND Builds.tsExpire > TestSets.tsCreated\n' \
+ ' AND Builds.tsEffective <= TestSets.tsCreated\n' \
+ ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \
+ + self.oFilter.getWhereConditions() \
+ + self.getExtraSubjectWhereExpr() + '\n' \
+ 'ORDER BY Builds.iRevision ' + sSorting + ',\n' \
+ ' TestSets.tsCreated ' + sSorting + '\n' \
+ 'LIMIT 1\n';
+ self._oDb.execute(sQuery);
+ aoRow = self._oDb.fetchOne();
+ if aoRow is None:
+ return ReportTransientBase(-1, -1, 'internal-error', -1, -1, self._oDb.getCurrentTimestamp(),
+ iPeriod, fEnter, idSubject, oSubject);
+ return ReportTransientBase(idBuild = aoRow[3], iRevision = aoRow[4], sRepository = aoRow[5],
+ idTestSet = aoRow[1], idTestResult = aoRow[0], tsDone = aoRow[2],
+ iPeriod = iPeriod, fEnter = fEnter, idSubject = idSubject, oSubject = oSubject);
+
+ def fetchPossibleFilterOptions(self, oFilter, tsNow, sPeriod):
+ """
+ Fetches possible filtering options.
+ """
+ return TestResultLogic(self._oDb).fetchPossibleFilterOptions(oFilter, tsNow, sPeriod, oReportModel = self);
+
+
+
+class ReportGraphModel(ReportModelBase): # pylint: disable=too-few-public-methods
+ """
+ Extended report model used when generating the more complicated graphs
+ detailing results, time elapsed and values over time.
+ """
+
+ ## @name Subject ID types.
+ ## These prefix the values in the aidSubjects array. The prefix is
+ ## followed by a colon and then a list of string IDs. Following the prefix
+ ## is one or more string table IDs separated by colons. These are used to
+ ## drill down the exact test result we're looking for, by matching against
+ ## TestResult::idStrName (in the db).
+ ## @{
+ ksTypeResult = 'result';
+ ksTypeElapsed = 'elapsed';
+ ## The last string table ID gives the name of the value.
+ ksTypeValue = 'value';
+ ## List of types.
+ kasTypes = (ksTypeResult, ksTypeElapsed, ksTypeValue);
+ ## @}
+
+ class SampleSource(object):
+ """ A sample source. """
+ def __init__(self, sType, aidStrTests, idStrValue):
+ self.sType = sType;
+ self.aidStrTests = aidStrTests;
+ self.idStrValue = idStrValue;
+
+ def getTestResultTables(self):
+ """ Retrieves the list of TestResults tables to join with."""
+ sRet = '';
+ for i in range(len(self.aidStrTests)):
+ sRet += ' TestResults TR%u,\n' % (i,);
+ return sRet;
+
+ def getTestResultConditions(self):
+ """ Retrieves the join conditions for the TestResults tables."""
+ sRet = '';
+ cItems = len(self.aidStrTests);
+ for i in range(cItems - 1):
+ sRet += ' AND TR%u.idStrName = %u\n' \
+ ' AND TR%u.idTestResultParent = TR%u.idTestResult\n' \
+ % ( i, self.aidStrTests[cItems - i - 1], i, i + 1 );
+ sRet += ' AND TR%u.idStrName = %u\n' % (cItems - 1, self.aidStrTests[0]);
+ return sRet;
+
+ class DataSeries(object):
+ """ A data series. """
+ def __init__(self, oCache, idBuildCategory, idTestBox, idTestCase, idTestCaseArgs, iUnit):
+ _ = oCache;
+ self.idBuildCategory = idBuildCategory;
+ self.oBuildCategory = oCache.getBuildCategory(idBuildCategory);
+ self.idTestBox = idTestBox;
+ self.oTestBox = oCache.getTestBox(idTestBox);
+ self.idTestCase = idTestCase;
+ self.idTestCaseArgs = idTestCaseArgs;
+ if idTestCase is not None:
+ self.oTestCase = oCache.getTestCase(idTestCase);
+ self.oTestCaseArgs = None;
+ else:
+ self.oTestCaseArgs = oCache.getTestCaseArgs(idTestCaseArgs);
+ self.oTestCase = oCache.getTestCase(self.oTestCaseArgs.idTestCase);
+ self.iUnit = iUnit;
+ # Six parallel arrays.
+ self.aiRevisions = []; # The X values.
+ self.aiValues = []; # The Y values.
+ self.aiErrorBarBelow = []; # The Y value minimum errorbars, relative to the Y value (positive).
+ self.aiErrorBarAbove = []; # The Y value maximum errorbars, relative to the Y value (positive).
+ self.acSamples = []; # The number of samples at this X value.
+ self.aoRevInfo = []; # VcsRevisionData objects for each revision. Empty/SQL-NULL objects if no info.
+
+ class DataSeriesCollection(object):
+ """ A collection of data series corresponding to one input sample source. """
+ def __init__(self, oSampleSrc, asTests, sValue = None):
+ self.sType = oSampleSrc.sType;
+ self.aidStrTests = oSampleSrc.aidStrTests;
+ self.asTests = list(asTests);
+ self.idStrValue = oSampleSrc.idStrValue;
+ self.sValue = sValue;
+ self.aoSeries = [];
+
+ def addDataSeries(self, oDataSeries):
+ """ Appends a data series to the collection. """
+ self.aoSeries.append(oDataSeries);
+ return oDataSeries;
+
+
+ def __init__(self, oDb, tsNow, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, # pylint: disable=too-many-arguments
+ aidTestBoxes, aidBuildCats, aidTestCases, fSepTestVars):
+ assert(sSubject == self.ksSubEverything); # dummy
+ ReportModelBase.__init__(self, oDb, tsNow, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter = None);
+ self.aidTestBoxes = aidTestBoxes;
+ self.aidBuildCats = aidBuildCats;
+ self.aidTestCases = aidTestCases;
+ self.fOnTestCase = not fSepTestVars; # (Separates testcase variations into separate data series.)
+ self.oCache = DatabaseObjCache(self._oDb, self.tsNow, None, self.cPeriods * self.cHoursPerPeriod);
+
+
+ # Quickly validate and convert the subject "IDs".
+ self.aoLookups = [];
+ for sCur in self.aidSubjects:
+ asParts = sCur.split(':');
+ if len(asParts) < 2:
+ raise TMExceptionBase('Invalid graph value "%s"' % (sCur,));
+
+ sType = asParts[0];
+ if sType not in ReportGraphModel.kasTypes:
+ raise TMExceptionBase('Invalid graph value type "%s" (full: "%s")' % (sType, sCur,));
+
+ aidStrTests = [];
+ for sIdStr in asParts[1:]:
+ try: idStr = int(sIdStr);
+ except: raise TMExceptionBase('Invalid graph value id "%s" (full: "%s")' % (sIdStr, sCur,));
+ if idStr < 0:
+ raise TMExceptionBase('Invalid graph value id "%u" (full: "%s")' % (idStr, sCur,));
+ aidStrTests.append(idStr);
+
+ idStrValue = None;
+ if sType == ReportGraphModel.ksTypeValue:
+ idStrValue = aidStrTests.pop();
+ self.aoLookups.append(ReportGraphModel.SampleSource(sType, aidStrTests, idStrValue));
+
+ # done
+
+
+ def getExtraWhereExprForTotalPeriod(self, sTimestampField):
+ """
+ Returns additional WHERE expression for getting test sets for the
+ specified period. It starts with an AND so that it can simply be
+ appended to the WHERE clause.
+ """
+ return self.getExtraWhereExprForTotalPeriodEx(sTimestampField, sTimestampField, True);
+
+ def getExtraWhereExprForTotalPeriodEx(self, sStartField = 'tsCreated', sEndField = 'tsDone', fLeadingAnd = True):
+ """
+ Returns additional WHERE expression for getting test sets for the
+ specified period.
+ """
+ if self.tsNow is None:
+ sNow = 'CURRENT_TIMESTAMP';
+ else:
+ sNow = self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,));
+
+ sRet = ' AND %s >= (%s - interval \'%u hours\')\n' \
+ ' AND %s <= %s\n' \
+ % ( sStartField, sNow, self.cPeriods * self.cHoursPerPeriod,
+ sEndField, sNow);
+
+ if not fLeadingAnd:
+ assert sRet[8] == ' ' and sRet[7] == 'D';
+ return sRet[9:];
+ return sRet;
+
+ def _getEligibleTestSetPeriod(self, sPrefix = 'TestSets.', fLeadingAnd = False):
+ """
+ Returns additional WHERE expression for getting TestSets rows
+ potentially relevant for the selected period.
+ """
+ if self.tsNow is None:
+ sNow = 'CURRENT_TIMESTAMP';
+ else:
+ sNow = self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,));
+
+ # The 2nd line is a performance hack on TestSets. It nudges postgresql
+ # into useing the TestSetsCreatedDoneIdx index instead of doing a table
+ # scan when we look for eligible bits there.
+ # ASSUMES no relevant test runs longer than 7 days!
+ sRet = ' AND %stsCreated <= %s\n' \
+ ' AND %stsCreated >= (%s - interval \'%u hours\' - interval \'%u days\')\n' \
+ ' AND %stsDone >= (%s - interval \'%u hours\')\n' \
+ % ( sPrefix, sNow,
+ sPrefix, sNow, self.cPeriods * self.cHoursPerPeriod, 7,
+ sPrefix, sNow, self.cPeriods * self.cHoursPerPeriod, );
+
+ if not fLeadingAnd:
+ assert sRet[8] == ' ' and sRet[7] == 'D';
+ return sRet[9:];
+ return sRet;
+
+
+ def _getNameStrings(self, aidStrTests):
+ """ Returns an array of names corresponding to the array of string table entries. """
+ return [self.oCache.getTestResultString(idStr) for idStr in aidStrTests];
+
+ def fetchGraphData(self):
+ """ returns data """
+ sWantedTestCaseId = 'idTestCase' if self.fOnTestCase else 'idTestCaseArgs';
+
+ aoRet = [];
+ for oLookup in self.aoLookups:
+ #
+ # Set up the result collection.
+ #
+ if oLookup.sType == self.ksTypeValue:
+ oCollection = self.DataSeriesCollection(oLookup, self._getNameStrings(oLookup.aidStrTests),
+ self.oCache.getTestResultString(oLookup.idStrValue));
+ else:
+ oCollection = self.DataSeriesCollection(oLookup, self._getNameStrings(oLookup.aidStrTests));
+
+ #
+ # Construct the query.
+ #
+ sQuery = 'SELECT Builds.iRevision,\n' \
+ ' TestSets.idBuildCategory,\n' \
+ ' TestSets.idTestBox,\n' \
+ ' TestSets.' + sWantedTestCaseId + ',\n';
+ if oLookup.sType == self.ksTypeValue:
+ sQuery += ' TestResultValues.iUnit as iUnit,\n' \
+ ' MIN(TestResultValues.lValue),\n' \
+ ' CAST(ROUND(AVG(TestResultValues.lValue)) AS BIGINT),\n' \
+ ' MAX(TestResultValues.lValue),\n' \
+ ' COUNT(TestResultValues.lValue)\n';
+ elif oLookup.sType == self.ksTypeElapsed:
+ sQuery += ' %u as iUnit,\n' \
+ ' CAST((EXTRACT(EPOCH FROM MIN(TR0.tsElapsed)) * 1000) AS INTEGER),\n' \
+ ' CAST((EXTRACT(EPOCH FROM AVG(TR0.tsElapsed)) * 1000) AS INTEGER),\n' \
+ ' CAST((EXTRACT(EPOCH FROM MAX(TR0.tsElapsed)) * 1000) AS INTEGER),\n' \
+ ' COUNT(TR0.tsElapsed)\n' \
+ % (constants.valueunit.MS,);
+ else:
+ sQuery += ' %u as iUnit,\n'\
+ ' MIN(TR0.cErrors),\n' \
+ ' CAST(ROUND(AVG(TR0.cErrors)) AS INTEGER),\n' \
+ ' MAX(TR0.cErrors),\n' \
+ ' COUNT(TR0.cErrors)\n' \
+ % (constants.valueunit.OCCURRENCES,);
+
+ if oLookup.sType == self.ksTypeValue:
+ sQuery += 'FROM TestResultValues,\n';
+ sQuery += ' TestSets,\n'
+ sQuery += oLookup.getTestResultTables();
+ else:
+ sQuery += 'FROM ' + oLookup.getTestResultTables().lstrip();
+ sQuery += ' TestSets,\n';
+ sQuery += ' Builds\n';
+
+ if oLookup.sType == self.ksTypeValue:
+ sQuery += 'WHERE TestResultValues.idStrName = %u\n' % ( oLookup.idStrValue, );
+ sQuery += self.getExtraWhereExprForTotalPeriod('TestResultValues.tsCreated');
+ sQuery += ' AND TestResultValues.idTestSet = TestSets.idTestSet\n';
+ sQuery += self._getEligibleTestSetPeriod(fLeadingAnd = True);
+ else:
+ sQuery += 'WHERE ' + (self.getExtraWhereExprForTotalPeriod('TR0.tsCreated').lstrip()[4:]).lstrip();
+ sQuery += ' AND TR0.idTestSet = TestSets.idTestSet\n';
+
+ if len(self.aidTestBoxes) == 1:
+ sQuery += ' AND TestSets.idTestBox = %u\n' % (self.aidTestBoxes[0],);
+ elif self.aidTestBoxes:
+ sQuery += ' AND TestSets.idTestBox IN (' + ','.join([str(i) for i in self.aidTestBoxes]) + ')\n';
+
+ if len(self.aidBuildCats) == 1:
+ sQuery += ' AND TestSets.idBuildCategory = %u\n' % (self.aidBuildCats[0],);
+ elif self.aidBuildCats:
+ sQuery += ' AND TestSets.idBuildCategory IN (' + ','.join([str(i) for i in self.aidBuildCats]) + ')\n';
+
+ if len(self.aidTestCases) == 1:
+ sQuery += ' AND TestSets.idTestCase = %u\n' % (self.aidTestCases[0],);
+ elif self.aidTestCases:
+ sQuery += ' AND TestSets.idTestCase IN (' + ','.join([str(i) for i in self.aidTestCases]) + ')\n';
+
+ if oLookup.sType == self.ksTypeElapsed:
+ sQuery += ' AND TestSets.enmStatus = \'%s\'::TestStatus_T\n' % (self.ksTestStatus_Success,);
+
+ if oLookup.sType == self.ksTypeValue:
+ sQuery += ' AND TestResultValues.idTestResult = TR0.idTestResult\n'
+ sQuery += self.getExtraWhereExprForTotalPeriod('TR0.tsCreated'); # For better index matching in some cases.
+
+ if oLookup.sType != self.ksTypeResult:
+ sQuery += ' AND TR0.enmStatus = \'%s\'::TestStatus_T\n' % (self.ksTestStatus_Success,);
+
+ sQuery += oLookup.getTestResultConditions();
+ sQuery += ' AND TestSets.idBuild = Builds.idBuild\n';
+
+ sQuery += 'GROUP BY TestSets.idBuildCategory,\n' \
+ ' TestSets.idTestBox,\n' \
+ ' TestSets.' + sWantedTestCaseId + ',\n' \
+ ' iUnit,\n' \
+ ' Builds.iRevision\n';
+ sQuery += 'ORDER BY TestSets.idBuildCategory,\n' \
+ ' TestSets.idTestBox,\n' \
+ ' TestSets.' + sWantedTestCaseId + ',\n' \
+ ' iUnit,\n' \
+ ' Builds.iRevision\n';
+
+ #
+ # Execute it and collect the result.
+ #
+ sCurRepository = None;
+ dRevisions = {};
+ oLastSeries = None;
+ idLastBuildCat = -1;
+ idLastTestBox = -1;
+ idLastTestCase = -1;
+ iLastUnit = -1;
+ self._oDb.execute(sQuery);
+ for aoRow in self._oDb.fetchAll(): # Fetching all here so we can make cache queries below.
+ if aoRow[1] != idLastBuildCat \
+ or aoRow[2] != idLastTestBox \
+ or aoRow[3] != idLastTestCase \
+ or aoRow[4] != iLastUnit:
+ idLastBuildCat = aoRow[1];
+ idLastTestBox = aoRow[2];
+ idLastTestCase = aoRow[3];
+ iLastUnit = aoRow[4];
+ if self.fOnTestCase:
+ oLastSeries = self.DataSeries(self.oCache, idLastBuildCat, idLastTestBox,
+ idLastTestCase, None, iLastUnit);
+ else:
+ oLastSeries = self.DataSeries(self.oCache, idLastBuildCat, idLastTestBox,
+ None, idLastTestCase, iLastUnit);
+ oCollection.addDataSeries(oLastSeries);
+ if oLastSeries.oBuildCategory.sRepository != sCurRepository:
+ if sCurRepository is not None:
+ self.oCache.preloadVcsRevInfo(sCurRepository, dRevisions.keys());
+ sCurRepository = oLastSeries.oBuildCategory.sRepository
+ dRevisions = {};
+ oLastSeries.aiRevisions.append(aoRow[0]);
+ oLastSeries.aiValues.append(aoRow[6]);
+ oLastSeries.aiErrorBarBelow.append(aoRow[6] - aoRow[5]);
+ oLastSeries.aiErrorBarAbove.append(aoRow[7] - aoRow[6]);
+ oLastSeries.acSamples.append(aoRow[8]);
+ dRevisions[aoRow[0]] = 1;
+
+ if sCurRepository is not None:
+ self.oCache.preloadVcsRevInfo(sCurRepository, dRevisions.keys());
+ del dRevisions;
+
+ #
+ # Look up the VCS revision details.
+ #
+ for oSeries in oCollection.aoSeries:
+ for iRevision in oSeries.aiRevisions:
+ oSeries.aoRevInfo.append(self.oCache.getVcsRevInfo(sCurRepository, iRevision));
+ aoRet.append(oCollection);
+
+ return aoRet;
+
+ def getEligibleTestBoxes(self):
+ """
+ Returns a list of TestBoxData objects with eligible testboxes for
+ the total period of time defined for this graph.
+ """
+
+ # Taking the simple way out now, getting all active testboxes at the
+ # time without filtering out on sample sources.
+
+ # 1. Collect the relevant testbox generation IDs.
+ self._oDb.execute('SELECT DISTINCT idTestBox, idGenTestBox\n'
+ 'FROM TestSets\n'
+ 'WHERE ' + self._getEligibleTestSetPeriod(fLeadingAnd = False) +
+ 'ORDER BY idTestBox, idGenTestBox DESC');
+ idPrevTestBox = -1;
+ asIdGenTestBoxes = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRow = self._oDb.fetchOne();
+ if aoRow[0] != idPrevTestBox:
+ idPrevTestBox = aoRow[0];
+ asIdGenTestBoxes.append(str(aoRow[1]));
+
+ # 2. Query all the testbox data in one go.
+ aoRet = [];
+ if asIdGenTestBoxes:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestBoxesWithStrings\n'
+ 'WHERE idGenTestBox IN (' + ','.join(asIdGenTestBoxes) + ')\n'
+ 'ORDER BY sName');
+ for _ in range(self._oDb.getRowCount()):
+ aoRet.append(TestBoxData().initFromDbRow(self._oDb.fetchOne()));
+
+ return aoRet;
+
+ def getEligibleBuildCategories(self):
+ """
+ Returns a list of BuildCategoryData objects with eligible build
+ categories for the total period of time defined for this graph. In
+ addition it will add any currently selected categories that aren't
+ really relevant to the period, just to simplify the WUI code.
+
+ """
+
+ # Taking the simple way out now, getting all used build cat without
+ # any testbox or testcase filtering.
+
+ sSelectedBuildCats = '';
+ if self.aidBuildCats:
+ sSelectedBuildCats = ' OR idBuildCategory IN (' + ','.join([str(i) for i in self.aidBuildCats]) + ')\n';
+
+ self._oDb.execute('SELECT DISTINCT *\n'
+ 'FROM BuildCategories\n'
+ 'WHERE idBuildCategory IN (\n'
+ ' SELECT DISTINCT idBuildCategory\n'
+ ' FROM TestSets\n'
+ ' WHERE ' + self._getEligibleTestSetPeriod(fLeadingAnd = False) +
+ ')\n'
+ + sSelectedBuildCats +
+ 'ORDER BY sProduct,\n'
+ ' sBranch,\n'
+ ' asOsArches,\n'
+ ' sType\n');
+ aoRet = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRet.append(BuildCategoryData().initFromDbRow(self._oDb.fetchOne()));
+
+ return aoRet;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/restdispatcher.py b/src/VBox/ValidationKit/testmanager/core/restdispatcher.py
new file mode 100755
index 00000000..75a1aa7c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/restdispatcher.py
@@ -0,0 +1,455 @@
+# -*- coding: utf-8 -*-
+# $Id: restdispatcher.py $
+
+"""
+Test Manager Core - REST cgi handler.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os;
+import sys;
+
+# Validation Kit imports.
+#from common import constants;
+from common import utils;
+from testmanager import config;
+#from testmanager.core import coreconsts;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.base import TMExceptionBase, ModelDataBase;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ long = int; # pylint: disable=redefined-builtin,invalid-name
+
+
+#
+# Exceptions
+#
+
+class RestDispException(TMExceptionBase):
+ """
+ Exception class for the REST dispatcher.
+ """
+ def __init__(self, sMsg, iStatus):
+ TMExceptionBase.__init__(self, sMsg);
+ self.iStatus = iStatus;
+
+# 400
+class RestDispException400(RestDispException):
+ """ A 400 error """
+ def __init__(self, sMsg):
+ RestDispException.__init__(self, sMsg, 400);
+
+class RestUnknownParameters(RestDispException400):
+ """ Unknown parameter(s). """
+ pass; # pylint: disable=unnecessary-pass
+
+# 404
+class RestDispException404(RestDispException):
+ """ A 404 error """
+ def __init__(self, sMsg):
+ RestDispException.__init__(self, sMsg, 404);
+
+class RestBadPathException(RestDispException404):
+ """ We've got a bad path. """
+ pass; # pylint: disable=unnecessary-pass
+
+class RestBadParameter(RestDispException404):
+ """ Bad parameter. """
+ pass; # pylint: disable=unnecessary-pass
+
+class RestMissingParameter(RestDispException404):
+ """ Missing parameter. """
+ pass; # pylint: disable=unnecessary-pass
+
+
+
+class RestMain(object): # pylint: disable=too-few-public-methods
+ """
+ REST main dispatcher class.
+ """
+
+ ksParam_sPath = 'sPath';
+
+
+ def __init__(self, oSrvGlue):
+ self._oSrvGlue = oSrvGlue;
+ self._oDb = TMDatabaseConnection(oSrvGlue.dprint);
+ self._iFirstHandlerPath = 0;
+ self._iNextHandlerPath = 0;
+ self._sPath = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._asPath = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._sMethod = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._dParams = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._asCheckedParams = [];
+ self._dGetTree = {
+ 'vcs': {
+ 'changelog': self._handleVcsChangelog_Get,
+ 'bugreferences': self._handleVcsBugReferences_Get,
+ },
+ };
+ self._dMethodTrees = {
+ 'GET': self._dGetTree,
+ }
+
+ #
+ # Helpers.
+ #
+
+ def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None):
+ """
+ Gets a string parameter (stripped).
+
+ Raises exception if not found and no default is provided, or if the
+ value isn't found in asValidValues.
+ """
+ if sName not in self._dParams:
+ if sDefValue is None:
+ raise RestMissingParameter('%s parameter %s is missing' % (self._sPath, sName));
+ return sDefValue;
+ sValue = self._dParams[sName];
+ if isinstance(sValue, list):
+ if len(sValue) == 1:
+ sValue = sValue[0];
+ else:
+ raise RestBadParameter('%s parameter %s value is not a string but list: %s'
+ % (self._sPath, sName, sValue));
+ if fStrip:
+ sValue = sValue.strip();
+
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ if asValidValues is not None and sValue not in asValidValues:
+ raise RestBadParameter('%s parameter %s value "%s" not in %s '
+ % (self._sPath, sName, sValue, asValidValues));
+ return sValue;
+
+ def _getBoolParam(self, sName, fDefValue = None):
+ """
+ Gets a boolean parameter.
+
+ Raises exception if not found and no default is provided, or if not a
+ valid boolean.
+ """
+ sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue));
+ return sValue in ('True', 'true', '1',);
+
+ def _getIntParam(self, sName, iMin = None, iMax = None):
+ """
+ Gets a string parameter.
+ Raises exception if not found, not a valid integer, or if the value
+ isn't in the range defined by iMin and iMax.
+ """
+ sValue = self._getStringParam(sName);
+ try:
+ iValue = int(sValue, 0);
+ except:
+ raise RestBadParameter('%s parameter %s value "%s" cannot be convert to an integer'
+ % (self._sPath, sName, sValue));
+
+ if (iMin is not None and iValue < iMin) \
+ or (iMax is not None and iValue > iMax):
+ raise RestBadParameter('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sPath, sName, iValue, iMin, iMax));
+ return iValue;
+
+ def _getLongParam(self, sName, lMin = None, lMax = None, lDefValue = None):
+ """
+ Gets a string parameter.
+ Raises exception if not found, not a valid long integer, or if the value
+ isn't in the range defined by lMin and lMax.
+ """
+ sValue = self._getStringParam(sName, sDefValue = (str(lDefValue) if lDefValue is not None else None));
+ try:
+ lValue = long(sValue, 0);
+ except Exception as oXcpt:
+ raise RestBadParameter('%s parameter %s value "%s" cannot be convert to an integer (%s)'
+ % (self._sPath, sName, sValue, oXcpt));
+
+ if (lMin is not None and lValue < lMin) \
+ or (lMax is not None and lValue > lMax):
+ raise RestBadParameter('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sPath, sName, lValue, lMin, lMax));
+ return lValue;
+
+ def _checkForUnknownParameters(self):
+ """
+ Check if we've handled all parameters, raises exception if anything
+ unknown was found.
+ """
+
+ if len(self._asCheckedParams) != len(self._dParams):
+ sUnknownParams = '';
+ for sKey in self._dParams:
+ if sKey not in self._asCheckedParams:
+ sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey];
+ raise RestUnknownParameters('Unknown parameters: ' + sUnknownParams);
+
+ return True;
+
+ def writeToMainLog(self, oTestSet, sText, fIgnoreSizeCheck = False):
+ """ Writes the text to the main log file. """
+
+ # Calc the file name and open the file.
+ sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-main.log');
+ if not os.path.exists(os.path.dirname(sFile)):
+ os.makedirs(os.path.dirname(sFile), 0o755);
+
+ with open(sFile, 'ab') as oFile:
+ # Check the size.
+ fSizeOk = True;
+ if not fIgnoreSizeCheck:
+ oStat = os.fstat(oFile.fileno());
+ fSizeOk = oStat.st_size / (1024 * 1024) < config.g_kcMbMaxMainLog;
+
+ # Write the text.
+ if fSizeOk:
+ if sys.version_info[0] >= 3:
+ oFile.write(bytes(sText, 'utf-8'));
+ else:
+ oFile.write(sText);
+
+ return fSizeOk;
+
+ def _getNextPathElementString(self, sName, oDefault = None):
+ """
+ Gets the next handler specific path element.
+ Returns unprocessed string.
+ Throws exception
+ """
+ i = self._iNextHandlerPath;
+ if i < len(self._asPath):
+ self._iNextHandlerPath = i + 1;
+ return self._asPath[i];
+ if oDefault is None:
+ raise RestBadPathException('Requires a "%s" element after "%s"' % (sName, self._sPath,));
+ return oDefault;
+
+ def _getNextPathElementInt(self, sName, iDefault = None, iMin = None, iMax = None):
+ """
+ Gets the next handle specific path element as an integer.
+ Returns integer value.
+ Throws exception if not found or not a valid integer.
+ """
+ sValue = self._getNextPathElementString(sName, oDefault = iDefault);
+ try:
+ iValue = int(sValue);
+ except:
+ raise RestBadPathException('Not an integer "%s" (%s)' % (sValue, sName,));
+ if iMin is not None and iValue < iMin:
+ raise RestBadPathException('Integer "%s" value (%s) is too small, min %s' % (sValue, sName, iMin));
+ if iMax is not None and iValue > iMax:
+ raise RestBadPathException('Integer "%s" value (%s) is too large, max %s' % (sValue, sName, iMax));
+ return iValue;
+
+ def _getNextPathElementLong(self, sName, iDefault = None, iMin = None, iMax = None):
+ """
+ Gets the next handle specific path element as a long integer.
+ Returns integer value.
+ Throws exception if not found or not a valid integer.
+ """
+ sValue = self._getNextPathElementString(sName, oDefault = iDefault);
+ try:
+ iValue = long(sValue);
+ except:
+ raise RestBadPathException('Not an integer "%s" (%s)' % (sValue, sName,));
+ if iMin is not None and iValue < iMin:
+ raise RestBadPathException('Integer "%s" value (%s) is too small, min %s' % (sValue, sName, iMin));
+ if iMax is not None and iValue > iMax:
+ raise RestBadPathException('Integer "%s" value (%s) is too large, max %s' % (sValue, sName, iMax));
+ return iValue;
+
+ def _checkNoMorePathElements(self):
+ """
+ Checks that there are no more path elements.
+ Throws exception if there are.
+ """
+ i = self._iNextHandlerPath;
+ if i < len(self._asPath):
+ raise RestBadPathException('Unknown subpath "%s" below "%s"' %
+ ('/'.join(self._asPath[i:]), '/'.join(self._asPath[:i]),));
+ return True;
+
+ def _doneParsingArguments(self):
+ """
+ Checks that there are no more path elements or unhandled parameters.
+ Throws exception if there are.
+ """
+ self._checkNoMorePathElements();
+ self._checkForUnknownParameters();
+ return True;
+
+ def _dataArrayToJsonReply(self, aoData, sName = 'aoData', dExtraFields = None, iStatus = 200):
+ """
+ Converts aoData into an array objects
+ return True.
+ """
+ self._oSrvGlue.setContentType('application/json');
+ self._oSrvGlue.setStatus(iStatus);
+ self._oSrvGlue.write(u'{\n');
+ if dExtraFields:
+ for sKey in dExtraFields:
+ self._oSrvGlue.write(u' "%s": %s,\n' % (sKey, ModelDataBase.genericToJson(dExtraFields[sKey]),));
+ self._oSrvGlue.write(u' "c%s": %u,\n' % (sName[2:],len(aoData),));
+ self._oSrvGlue.write(u' "%s": [\n' % (sName,));
+ for i, oData in enumerate(aoData):
+ if i > 0:
+ self._oSrvGlue.write(u',\n');
+ self._oSrvGlue.write(ModelDataBase.genericToJson(oData));
+ self._oSrvGlue.write(u' ]\n');
+ ## @todo if config.g_kfWebUiSqlDebug:
+ self._oSrvGlue.write(u'}\n');
+ self._oSrvGlue.flush();
+ return True;
+
+
+ #
+ # Handlers.
+ #
+
+ def _handleVcsChangelog_Get(self):
+ """ GET /vcs/changelog/{sRepository}/{iStartRev}[/{cEntriesBack}] """
+ # Parse arguments
+ sRepository = self._getNextPathElementString('sRepository');
+ iStartRev = self._getNextPathElementInt('iStartRev', iMin = 0);
+ cEntriesBack = self._getNextPathElementInt('cEntriesBack', iDefault = 32, iMin = 0, iMax = 8192);
+ self._checkNoMorePathElements();
+ self._checkForUnknownParameters();
+
+ # Execute it.
+ from testmanager.core.vcsrevisions import VcsRevisionLogic;
+ oLogic = VcsRevisionLogic(self._oDb);
+ return self._dataArrayToJsonReply(oLogic.fetchTimeline(sRepository, iStartRev, cEntriesBack), 'aoCommits',
+ { 'sTracChangesetUrlFmt':
+ config.g_ksTracChangsetUrlFmt.replace('%(sRepository)s', sRepository), } );
+
+ def _handleVcsBugReferences_Get(self):
+ """ GET /vcs/bugreferences/{sTrackerId}/{lBugId} """
+ # Parse arguments
+ sTrackerId = self._getNextPathElementString('sTrackerId');
+ lBugId = self._getNextPathElementLong('lBugId', iMin = 0);
+ self._checkNoMorePathElements();
+ self._checkForUnknownParameters();
+
+ # Execute it.
+ from testmanager.core.vcsbugreference import VcsBugReferenceLogic;
+ oLogic = VcsBugReferenceLogic(self._oDb);
+ oLogic.fetchForBug(sTrackerId, lBugId)
+ return self._dataArrayToJsonReply(oLogic.fetchForBug(sTrackerId, lBugId), 'aoCommits',
+ { 'sTracChangesetUrlFmt': config.g_ksTracChangsetUrlFmt, } );
+
+
+ #
+ # Dispatching.
+ #
+
+ def _dispatchRequestCommon(self):
+ """
+ Dispatches the incoming request after have gotten the path and parameters.
+
+ Will raise RestDispException on failure.
+ """
+
+ #
+ # Split up the path.
+ #
+ asPath = self._sPath.split('/');
+ self._asPath = asPath;
+
+ #
+ # Get the method and the corresponding handler tree.
+ #
+ try:
+ sMethod = self._oSrvGlue.getMethod();
+ except Exception as oXcpt:
+ raise RestDispException('Error retriving request method: %s' % (oXcpt,), 400);
+ self._sMethod = sMethod;
+
+ try:
+ dTree = self._dMethodTrees[sMethod];
+ except KeyError:
+ raise RestDispException('Unsupported method %s' % (sMethod,), 405);
+
+ #
+ # Walk the path till we find a handler for it.
+ #
+ iPath = 0;
+ while iPath < len(asPath):
+ try:
+ oTreeOrHandler = dTree[asPath[iPath]];
+ except KeyError:
+ raise RestBadPathException('Path element #%u "%s" not found (path="%s")' % (iPath, asPath[iPath], self._sPath));
+ iPath += 1;
+ if isinstance(oTreeOrHandler, dict):
+ dTree = oTreeOrHandler;
+ else:
+ #
+ # Call the handler.
+ #
+ self._iFirstHandlerPath = iPath;
+ self._iNextHandlerPath = iPath;
+ return oTreeOrHandler();
+
+ raise RestBadPathException('Empty path (%s)' % (self._sPath,));
+
+ def dispatchRequest(self):
+ """
+ Dispatches the incoming request where the path is given as an argument.
+
+ Will raise RestDispException on failure.
+ """
+
+ #
+ # Get the parameters.
+ #
+ try:
+ dParams = self._oSrvGlue.getParameters();
+ except Exception as oXcpt:
+ raise RestDispException('Error retriving parameters: %s' % (oXcpt,), 500);
+ self._dParams = dParams;
+
+ #
+ # Get the path parameter.
+ #
+ if self.ksParam_sPath not in dParams:
+ raise RestDispException('No "%s" parameter in request (params: %s)' % (self.ksParam_sPath, dParams,), 500);
+ self._sPath = self._getStringParam(self.ksParam_sPath);
+ assert utils.isString(self._sPath);
+
+ return self._dispatchRequestCommon();
+
diff --git a/src/VBox/ValidationKit/testmanager/core/schedgroup.py b/src/VBox/ValidationKit/testmanager/core/schedgroup.py
new file mode 100755
index 00000000..a586a6bf
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/schedgroup.py
@@ -0,0 +1,1352 @@
+# -*- coding: utf-8 -*-
+# $Id: schedgroup.py $
+
+"""
+Test Manager - Scheduling Group.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \
+ TMRowInUse, TMInvalidData, TMRowAlreadyExists, TMRowNotFound, \
+ ChangeLogEntry, AttributeChangeEntry, AttributeChangeEntryPre;
+from testmanager.core.buildsource import BuildSourceData;
+from testmanager.core import db;
+from testmanager.core.testcase import TestCaseData;
+from testmanager.core.testcaseargs import TestCaseArgsData;
+from testmanager.core.testbox import TestBoxLogic, TestBoxDataForSchedGroup;
+from testmanager.core.testgroup import TestGroupData;
+from testmanager.core.useraccount import UserAccountLogic;
+
+
+
+class SchedGroupMemberData(ModelDataBase):
+ """
+ SchedGroupMember Data.
+ """
+
+ ksIdAttr = 'idSchedGroup';
+
+ ksParam_idSchedGroup = 'SchedGroupMember_idSchedGroup';
+ ksParam_idTestGroup = 'SchedGroupMember_idTestGroup';
+ ksParam_tsEffective = 'SchedGroupMember_tsEffective';
+ ksParam_tsExpire = 'SchedGroupMember_tsExpire';
+ ksParam_uidAuthor = 'SchedGroupMember_uidAuthor';
+ ksParam_iSchedPriority = 'SchedGroupMember_iSchedPriority';
+ ksParam_bmHourlySchedule = 'SchedGroupMember_bmHourlySchedule';
+ ksParam_idTestGroupPreReq = 'SchedGroupMember_idTestGroupPreReq';
+
+ kasAllowNullAttributes = [ 'idSchedGroup', 'idTestGroup', 'tsEffective', 'tsExpire',
+ 'uidAuthor', 'bmHourlySchedule', 'idTestGroupPreReq' ];
+ kiMin_iSchedPriority = 0;
+ kiMax_iSchedPriority = 32;
+
+ kcDbColumns = 8
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idSchedGroup = None;
+ self.idTestGroup = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.iSchedPriority = 16;
+ self.bmHourlySchedule = None;
+ self.idTestGroupPreReq = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the data with a row from a SELECT * FROM SchedGroupMembers.
+
+ Returns self. Raises exception if the row is None or otherwise invalid.
+ """
+
+ if aoRow is None:
+ raise TMRowNotFound('SchedGroupMember not found.');
+
+ self.idSchedGroup = aoRow[0];
+ self.idTestGroup = aoRow[1];
+ self.tsEffective = aoRow[2];
+ self.tsExpire = aoRow[3];
+ self.uidAuthor = aoRow[4];
+ self.iSchedPriority = aoRow[5];
+ self.bmHourlySchedule = aoRow[6]; ## @todo figure out how bitmaps are returned...
+ self.idTestGroupPreReq = aoRow[7];
+ return self;
+
+
+class SchedGroupMemberDataEx(SchedGroupMemberData):
+ """
+ Extended SchedGroupMember data class.
+ This adds the testgroups.
+ """
+
+ def __init__(self):
+ SchedGroupMemberData.__init__(self);
+ self.oTestGroup = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the data with a row from a query like this:
+
+ SELECT SchedGroupMembers.*, TestGroups.*
+ FROM SchedGroupMembers
+ JOIN TestGroups
+ ON (SchedGroupMembers.idTestGroup = TestGroups.idTestGroup);
+
+ Returns self. Raises exception if the row is None or otherwise invalid.
+ """
+ SchedGroupMemberData.initFromDbRow(self, aoRow);
+ self.oTestGroup = TestGroupData().initFromDbRow(aoRow[SchedGroupMemberData.kcDbColumns:]);
+ return self;
+
+ def getDataAttributes(self):
+ asAttributes = SchedGroupMemberData.getDataAttributes(self);
+ asAttributes.remove('oTestGroup');
+ return asAttributes;
+
+ def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other):
+ dErrors = SchedGroupMemberData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor);
+ if self.ksParam_idTestGroup not in dErrors:
+ self.oTestGroup = TestGroupData();
+ try:
+ self.oTestGroup.initFromDbWithId(oDb, self.idTestGroup);
+ except Exception as oXcpt:
+ self.oTestGroup = TestGroupData()
+ dErrors[self.ksParam_idTestGroup] = str(oXcpt);
+ return dErrors;
+
+
+
+
+class SchedGroupData(ModelDataBase):
+ """
+ SchedGroup Data.
+ """
+
+ ## @name TestBoxState_T
+ # @{
+ ksScheduler_BestEffortContinuousIntegration = 'bestEffortContinousItegration'; # sic*2
+ ksScheduler_Reserved = 'reserved';
+ ## @}
+
+
+ ksIdAttr = 'idSchedGroup';
+
+ ksParam_idSchedGroup = 'SchedGroup_idSchedGroup';
+ ksParam_tsEffective = 'SchedGroup_tsEffective';
+ ksParam_tsExpire = 'SchedGroup_tsExpire';
+ ksParam_uidAuthor = 'SchedGroup_uidAuthor';
+ ksParam_sName = 'SchedGroup_sName';
+ ksParam_sDescription = 'SchedGroup_sDescription';
+ ksParam_fEnabled = 'SchedGroup_fEnabled';
+ ksParam_enmScheduler = 'SchedGroup_enmScheduler';
+ ksParam_idBuildSrc = 'SchedGroup_idBuildSrc';
+ ksParam_idBuildSrcTestSuite = 'SchedGroup_idBuildSrcTestSuite';
+ ksParam_sComment = 'SchedGroup_sComment';
+
+ kasAllowNullAttributes = ['idSchedGroup', 'tsEffective', 'tsExpire', 'uidAuthor', 'sDescription',
+ 'idBuildSrc', 'idBuildSrcTestSuite', 'sComment' ];
+ kasValidValues_enmScheduler = [ ksScheduler_BestEffortContinuousIntegration, ];
+
+ kcDbColumns = 11;
+
+ # Scheduler types
+ kasSchedulerDesc = \
+ [
+ ( ksScheduler_BestEffortContinuousIntegration, 'Best-Effort-Continuous-Integration (BECI) scheduler.', ''),
+ ]
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idSchedGroup = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.sName = None;
+ self.sDescription = None;
+ self.fEnabled = None;
+ self.enmScheduler = SchedGroupData.ksScheduler_BestEffortContinuousIntegration;
+ self.idBuildSrc = None;
+ self.idBuildSrcTestSuite = None;
+ self.sComment = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the data with a row from a SELECT * FROM SchedGroups.
+
+ Returns self. Raises exception if the row is None or otherwise invalid.
+ """
+
+ if aoRow is None:
+ raise TMRowNotFound('SchedGroup not found.');
+
+ self.idSchedGroup = aoRow[0];
+ self.tsEffective = aoRow[1];
+ self.tsExpire = aoRow[2];
+ self.uidAuthor = aoRow[3];
+ self.sName = aoRow[4];
+ self.sDescription = aoRow[5];
+ self.fEnabled = aoRow[6];
+ self.enmScheduler = aoRow[7];
+ self.idBuildSrc = aoRow[8];
+ self.idBuildSrcTestSuite = aoRow[9];
+ self.sComment = aoRow[10];
+ return self;
+
+ def initFromDbWithId(self, oDb, idSchedGroup, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE idSchedGroup = %s\n'
+ , ( idSchedGroup,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idSchedGroup=%s not found (tsNow=%s, sPeriodBack=%s)' % (idSchedGroup, tsNow, sPeriodBack));
+ return self.initFromDbRow(aoRow);
+
+
+class SchedGroupDataEx(SchedGroupData):
+ """
+ Extended scheduling group data.
+
+ Note! Similar to TestGroupDataEx.
+ """
+
+ ksParam_aoMembers = 'SchedGroup_aoMembers';
+ ksParam_aoTestBoxes = 'SchedGroup_aoTestboxes';
+ kasAltArrayNull = [ 'aoMembers', 'aoTestboxes' ];
+
+ ## Helper parameter containing the comma separated list with the IDs of
+ # potential members found in the parameters.
+ ksParam_aidTestGroups = 'TestGroupDataEx_aidTestGroups';
+ ## Ditto for testbox meembers.
+ ksParam_aidTestBoxes = 'TestGroupDataEx_aidTestBoxes';
+
+
+ def __init__(self):
+ SchedGroupData.__init__(self);
+ self.aoMembers = [] # type: list[SchedGroupMemberDataEx]
+ self.aoTestBoxes = [] # type: list[TestBoxDataForSchedGroup]
+
+ # The two build sources for the sake of convenience.
+ self.oBuildSrc = None # type: BuildSourceData
+ self.oBuildSrcValidationKit = None # type: BuildSourceData
+
+ def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None):
+ """
+ Worker shared by the initFromDb* methods.
+ Returns self. Raises exception if no row or database error.
+ """
+ #
+ # Clear all members upfront so the object has some kind of consistency
+ # if anything below raises exceptions.
+ #
+ self.oBuildSrc = None;
+ self.oBuildSrcValidationKit = None;
+ self.aoTestBoxes = [];
+ self.aoMembers = [];
+
+ #
+ # Build source.
+ #
+ if self.idBuildSrc:
+ self.oBuildSrc = BuildSourceData().initFromDbWithId(oDb, self.idBuildSrc, tsNow, sPeriodBack);
+
+ if self.idBuildSrcTestSuite:
+ self.oBuildSrcValidationKit = BuildSourceData().initFromDbWithId(oDb, self.idBuildSrcTestSuite,
+ tsNow, sPeriodBack);
+
+ #
+ # Test Boxes.
+ #
+ self.aoTestBoxes = TestBoxLogic(oDb).fetchForSchedGroup(self.idSchedGroup, tsNow);
+
+ #
+ # Test groups.
+ # The fetchForChangeLog method makes ASSUMPTIONS about sorting!
+ #
+ oDb.execute('SELECT SchedGroupMembers.*, TestGroups.*\n'
+ 'FROM SchedGroupMembers\n'
+ 'LEFT OUTER JOIN TestGroups ON (SchedGroupMembers.idTestGroup = TestGroups.idTestGroup)\n'
+ 'WHERE SchedGroupMembers.idSchedGroup = %s\n'
+ + self.formatSimpleNowAndPeriod(oDb, tsNow, sPeriodBack, sTablePrefix = 'SchedGroupMembers.')
+ + self.formatSimpleNowAndPeriod(oDb, tsNow, sPeriodBack, sTablePrefix = 'TestGroups.') +
+ 'ORDER BY SchedGroupMembers.idTestGroupPreReq ASC NULLS FIRST,\n'
+ ' TestGroups.sName,\n'
+ ' SchedGroupMembers.idTestGroup\n'
+ , (self.idSchedGroup,));
+ for aoRow in oDb.fetchAll():
+ self.aoMembers.append(SchedGroupMemberDataEx().initFromDbRow(aoRow));
+ return self;
+
+ def initFromDbRowEx(self, aoRow, oDb, tsNow = None):
+ """
+ Reinitialize from a SELECT * FROM SchedGroups row. Will query the
+ necessary additional data from oDb using tsNow.
+ Returns self. Raises exception if no row or database error.
+ """
+ SchedGroupData.initFromDbRow(self, aoRow);
+ return self._initExtraMembersFromDb(oDb, tsNow);
+
+ def initFromDbWithId(self, oDb, idSchedGroup, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ SchedGroupData.initFromDbWithId(self, oDb, idSchedGroup, tsNow, sPeriodBack);
+ return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack);
+
+ def getDataAttributes(self):
+ asAttributes = SchedGroupData.getDataAttributes(self);
+ asAttributes.remove('oBuildSrc');
+ asAttributes.remove('oBuildSrcValidationKit');
+ return asAttributes;
+
+ def getAttributeParamNullValues(self, sAttr):
+ if sAttr not in [ 'aoMembers', 'aoTestBoxes' ]:
+ return SchedGroupData.getAttributeParamNullValues(self, sAttr);
+ return ['', [], None];
+
+ def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
+ aoNewValue = [];
+ if sAttr == 'aoMembers':
+ aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = [])
+ sIds = oDisp.getStringParam(self.ksParam_aidTestGroups, sDefault = '');
+ for idTestGroup in sIds.split(','):
+ try: idTestGroup = int(idTestGroup);
+ except: pass;
+ oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (SchedGroupDataEx.ksParam_aoMembers, idTestGroup,))
+ oMember = SchedGroupMemberDataEx().initFromParams(oDispWrapper, fStrict = False);
+ if idTestGroup in aidSelected:
+ oMember.idTestGroup = idTestGroup;
+ aoNewValue.append(oMember);
+ elif sAttr == 'aoTestBoxes':
+ aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = [])
+ sIds = oDisp.getStringParam(self.ksParam_aidTestBoxes, sDefault = '');
+ for idTestBox in sIds.split(','):
+ try: idTestBox = int(idTestBox);
+ except: pass;
+ oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (SchedGroupDataEx.ksParam_aoTestBoxes, idTestBox,))
+ oBoxInGrp = TestBoxDataForSchedGroup().initFromParams(oDispWrapper, fStrict = False);
+ if idTestBox in aidSelected:
+ oBoxInGrp.idTestBox = idTestBox;
+ aoNewValue.append(oBoxInGrp);
+ else:
+ return SchedGroupData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict);
+ return aoNewValue;
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ if sAttr not in [ 'aoMembers', 'aoTestBoxes' ]:
+ return SchedGroupData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+
+ if oValue in aoNilValues:
+ return ([], None);
+
+ asErrors = [];
+ aoNewMembers = [];
+ if sAttr == 'aoMembers':
+ asAllowNulls = ['bmHourlySchedule', 'idTestGroupPreReq', 'tsEffective', 'tsExpire', 'uidAuthor', ];
+ if self.idSchedGroup in [None, '-1', -1]:
+ asAllowNulls.append('idSchedGroup'); # Probably new group, so allow null scheduling group.
+
+ for oOldMember in oValue:
+ oNewMember = SchedGroupMemberDataEx().initFromOther(oOldMember);
+ aoNewMembers.append(oNewMember);
+
+ dErrors = oNewMember.validateAndConvertEx(asAllowNulls, oDb, ModelDataBase.ksValidateFor_Other);
+ if dErrors:
+ asErrors.append(str(dErrors));
+
+ if not asErrors:
+ for i, _ in enumerate(aoNewMembers):
+ idTestGroup = aoNewMembers[i];
+ for j in range(i + 1, len(aoNewMembers)):
+ if aoNewMembers[j].idTestGroup == idTestGroup:
+ asErrors.append('Duplicate test group #%d!' % (idTestGroup, ));
+ break;
+ else:
+ asAllowNulls = list(TestBoxDataForSchedGroup.kasAllowNullAttributes);
+ if self.idSchedGroup in [None, '-1', -1]:
+ asAllowNulls.append('idSchedGroup'); # Probably new group, so allow null scheduling group.
+
+ for oOldMember in oValue:
+ oNewMember = TestBoxDataForSchedGroup().initFromOther(oOldMember);
+ aoNewMembers.append(oNewMember);
+
+ dErrors = oNewMember.validateAndConvertEx(asAllowNulls, oDb, ModelDataBase.ksValidateFor_Other);
+ if dErrors:
+ asErrors.append(str(dErrors));
+
+ if not asErrors:
+ for i, _ in enumerate(aoNewMembers):
+ idTestBox = aoNewMembers[i];
+ for j in range(i + 1, len(aoNewMembers)):
+ if aoNewMembers[j].idTestBox == idTestBox:
+ asErrors.append('Duplicate test box #%d!' % (idTestBox, ));
+ break;
+
+ return (aoNewMembers, None if not asErrors else '<br>\n'.join(asErrors));
+
+ def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other):
+ dErrors = SchedGroupData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor);
+
+ #
+ # Fetch the extended build source bits.
+ #
+ if self.ksParam_idBuildSrc not in dErrors:
+ if self.idBuildSrc in self.getAttributeParamNullValues('idBuildSrc') \
+ or self.idBuildSrc is None:
+ self.oBuildSrc = None;
+ else:
+ try:
+ self.oBuildSrc = BuildSourceData().initFromDbWithId(oDb, self.idBuildSrc);
+ except Exception as oXcpt:
+ self.oBuildSrc = BuildSourceData();
+ dErrors[self.ksParam_idBuildSrc] = str(oXcpt);
+
+ if self.ksParam_idBuildSrcTestSuite not in dErrors:
+ if self.idBuildSrcTestSuite in self.getAttributeParamNullValues('idBuildSrcTestSuite') \
+ or self.idBuildSrcTestSuite is None:
+ self.oBuildSrcValidationKit = None;
+ else:
+ try:
+ self.oBuildSrcValidationKit = BuildSourceData().initFromDbWithId(oDb, self.idBuildSrcTestSuite);
+ except Exception as oXcpt:
+ self.oBuildSrcValidationKit = BuildSourceData();
+ dErrors[self.ksParam_idBuildSrcTestSuite] = str(oXcpt);
+
+ return dErrors;
+
+
+
+class SchedGroupLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ SchedGroup logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb);
+ self.dCache = None;
+
+ #
+ # Standard methods.
+ #
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches build sources.
+
+ Returns an array (list) of BuildSourceData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY fEnabled DESC, sName DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY fEnabled DESC, sName DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(SchedGroupDataEx().initFromDbRowEx(aoRow, self._oDb, tsNow));
+ return aoRet;
+
+ def fetchForChangeLog(self, idSchedGroup, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals,too-many-statements
+ """
+ Fetches change log entries for a scheduling group.
+
+ Returns an array of ChangeLogEntry instance and an indicator whether
+ there are more entries.
+ Raises exception on error.
+ """
+
+ ## @todo calc changes to scheduler group!
+
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+
+ #
+ # First gather the change log timeline using the effective dates.
+ # (ASSUMES that we'll always have a separate delete entry, rather
+ # than just setting tsExpire.)
+ #
+ self._oDb.execute('''
+(
+SELECT tsEffective,
+ uidAuthor
+FROM SchedGroups
+WHERE idSchedGroup = %s
+ AND tsEffective <= %s
+ORDER BY tsEffective DESC
+) UNION (
+SELECT CASE WHEN tsEffective + %s::INTERVAL = tsExpire THEN tsExpire ELSE tsEffective END,
+ uidAuthor
+FROM SchedGroupMembers
+WHERE idSchedGroup = %s
+ AND tsEffective <= %s
+ORDER BY tsEffective DESC
+) UNION (
+SELECT CASE WHEN tsEffective + %s::INTERVAL = tsExpire THEN tsExpire ELSE tsEffective END,
+ uidAuthor
+FROM TestBoxesInSchedGroups
+WHERE idSchedGroup = %s
+ AND tsEffective <= %s
+ORDER BY tsEffective DESC
+)
+ORDER BY tsEffective DESC
+LIMIT %s OFFSET %s
+''', (idSchedGroup, tsNow,
+ db.dbOneTickIntervalString(), idSchedGroup, tsNow,
+ db.dbOneTickIntervalString(), idSchedGroup, tsNow,
+ cMaxRows + 1, iStart, ));
+
+ aoEntries = [] # type: list[ChangeLogEntry]
+ tsPrevious = tsNow;
+ for aoDbRow in self._oDb.fetchAll():
+ (tsEffective, uidAuthor) = aoDbRow;
+ aoEntries.append(ChangeLogEntry(uidAuthor, None, tsEffective, tsPrevious, None, None, []));
+ tsPrevious = db.dbTimestampPlusOneTick(tsEffective);
+
+ if True: # pylint: disable=using-constant-test
+ #
+ # Fetch data for each for each change log entry point.
+ #
+ # We add one tick to the timestamp here to skip past delete records
+ # that only there to record the user doing the deletion.
+ #
+ for iEntry, oEntry in enumerate(aoEntries):
+ oEntry.oNewRaw = SchedGroupDataEx().initFromDbWithId(self._oDb, idSchedGroup, oEntry.tsEffective);
+ if iEntry > 0:
+ aoEntries[iEntry - 1].oOldRaw = oEntry.oNewRaw;
+
+ # Chop off the +1 entry, if any.
+ fMore = len(aoEntries) > cMaxRows;
+ if fMore:
+ aoEntries = aoEntries[:-1];
+
+ # Figure out the changes.
+ for oEntry in aoEntries:
+ oOld = oEntry.oOldRaw;
+ if not oOld:
+ break;
+ oNew = oEntry.oNewRaw;
+ aoChanges = oEntry.aoChanges;
+ for sAttr in oNew.getDataAttributes():
+ if sAttr in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+ continue;
+ oOldAttr = getattr(oOld, sAttr);
+ oNewAttr = getattr(oNew, sAttr);
+ if oOldAttr == oNewAttr:
+ continue;
+ if sAttr in [ 'aoMembers', 'aoTestBoxes', ]:
+ iNew = 0;
+ iOld = 0;
+ asNewAttr = [];
+ asOldAttr = [];
+ if sAttr == 'aoMembers':
+ # ASSUMES aoMembers is sorted by idTestGroupPreReq (nulls first), oTestGroup.sName, idTestGroup!
+ while iNew < len(oNewAttr) and iOld < len(oOldAttr):
+ if oNewAttr[iNew].idTestGroup == oOldAttr[iOld].idTestGroup:
+ if oNewAttr[iNew].idTestGroupPreReq != oOldAttr[iOld].idTestGroupPreReq:
+ if oNewAttr[iNew].idTestGroupPreReq is None:
+ asOldAttr.append('Dropped test group #%s (%s) dependency on #%s'
+ % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName,
+ oOldAttr[iOld].idTestGroupPreReq));
+ elif oOldAttr[iOld].idTestGroupPreReq is None:
+ asNewAttr.append('Added test group #%s (%s) dependency on #%s'
+ % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName,
+ oNewAttr[iOld].idTestGroupPreReq));
+ else:
+ asNewAttr.append('Test group #%s (%s) dependency on #%s'
+ % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName,
+ oNewAttr[iNew].idTestGroupPreReq));
+ asOldAttr.append('Test group #%s (%s) dependency on #%s'
+ % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName,
+ oOldAttr[iOld].idTestGroupPreReq));
+ if oNewAttr[iNew].iSchedPriority != oOldAttr[iOld].iSchedPriority:
+ asNewAttr.append('Test group #%s (%s) priority %s'
+ % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName,
+ oNewAttr[iNew].iSchedPriority));
+ asOldAttr.append('Test group #%s (%s) priority %s'
+ % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName,
+ oOldAttr[iOld].iSchedPriority));
+ iNew += 1;
+ iOld += 1;
+ elif oNewAttr[iNew].oTestGroup.sName < oOldAttr[iOld].oTestGroup.sName \
+ or ( oNewAttr[iNew].oTestGroup.sName == oOldAttr[iOld].oTestGroup.sName
+ and oNewAttr[iNew].idTestGroup < oOldAttr[iOld].idTestGroup):
+ asNewAttr.append('New test group #%s - %s'
+ % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName));
+ iNew += 1;
+ else:
+ asOldAttr.append('Removed test group #%s - %s'
+ % (oOldAttr[iOld].idTestGroup, oOldAttr[iOld].oTestGroup.sName));
+ iOld += 1;
+ while iNew < len(oNewAttr):
+ asNewAttr.append('New test group #%s - %s'
+ % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName));
+ iNew += 1;
+ while iOld < len(oOldAttr):
+ asOldAttr.append('Removed test group #%s - %s'
+ % (oOldAttr[iOld].idTestGroup, oOldAttr[iOld].oTestGroup.sName));
+ iOld += 1;
+ else:
+ dNewIds = { oBoxInGrp.idTestBox: oBoxInGrp for oBoxInGrp in oNewAttr };
+ dOldIds = { oBoxInGrp.idTestBox: oBoxInGrp for oBoxInGrp in oOldAttr };
+ hCommonIds = set(dNewIds.keys()) & set(dOldIds.keys());
+ for idTestBox in hCommonIds:
+ oNewBoxInGrp = dNewIds[idTestBox];
+ oOldBoxInGrp = dOldIds[idTestBox];
+ if oNewBoxInGrp.iSchedPriority != oOldBoxInGrp.iSchedPriority:
+ asNewAttr.append('Test box \'%s\' (#%s) priority %s'
+ % (getattr(oNewBoxInGrp.oTestBox, 'sName', '[Partial DB]'),
+ oNewBoxInGrp.idTestBox, oNewBoxInGrp.iSchedPriority));
+ asOldAttr.append('Test box \'%s\' (#%s) priority %s'
+ % (getattr(oOldBoxInGrp.oTestBox, 'sName', '[Partial DB]'),
+ oOldBoxInGrp.idTestBox, oOldBoxInGrp.iSchedPriority));
+ asNewAttr = sorted(asNewAttr);
+ asOldAttr = sorted(asOldAttr);
+ for idTestBox in set(dNewIds.keys()) - hCommonIds:
+ oNewBoxInGrp = dNewIds[idTestBox];
+ asNewAttr.append('New test box \'%s\' (#%s) priority %s'
+ % (getattr(oNewBoxInGrp.oTestBox, 'sName', '[Partial DB]'),
+ oNewBoxInGrp.idTestBox, oNewBoxInGrp.iSchedPriority));
+ for idTestBox in set(dOldIds.keys()) - hCommonIds:
+ oOldBoxInGrp = dOldIds[idTestBox];
+ asOldAttr.append('Removed test box \'%s\' (#%s) priority %s'
+ % (getattr(oOldBoxInGrp.oTestBox, 'sName', '[Partial DB]'),
+ oOldBoxInGrp.idTestBox, oOldBoxInGrp.iSchedPriority));
+
+ if asNewAttr or asOldAttr:
+ aoChanges.append(AttributeChangeEntryPre(sAttr, oNewAttr, oOldAttr,
+ '\n'.join(asNewAttr), '\n'.join(asOldAttr)));
+ else:
+ aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+
+ else:
+ ##
+ ## @todo Incomplete: A more complicate apporach, probably faster though.
+ ##
+ def findEntry(tsEffective, iPrev = 0):
+ """ Find entry with matching effective + expiration time """
+ self._oDb.dprint('findEntry: iPrev=%s len(aoEntries)=%s tsEffective=%s' % (iPrev, len(aoEntries), tsEffective));
+ while iPrev < len(aoEntries):
+ self._oDb.dprint('%s iPrev=%u' % (aoEntries[iPrev].tsEffective, iPrev, ));
+ if aoEntries[iPrev].tsEffective > tsEffective:
+ iPrev += 1;
+ elif aoEntries[iPrev].tsEffective == tsEffective:
+ self._oDb.dprint('hit %u' % (iPrev,));
+ return iPrev;
+ else:
+ break;
+ self._oDb.dprint('%s not found!' % (tsEffective,));
+ return -1;
+
+ fMore = True;
+
+ #
+ # Track scheduling group changes. Not terribly efficient for large cMaxRows
+ # values, but not in the mood for figure out if there is any way to optimize that.
+ #
+ self._oDb.execute('''
+SELECT *
+FROM SchedGroups
+WHERE idSchedGroup = %s
+ AND tsEffective <= %s
+ORDER BY tsEffective DESC
+LIMIT %s''', (idSchedGroup, aoEntries[0].tsEffective, cMaxRows + 1,));
+
+ iEntry = 0;
+ aaoRows = self._oDb.fetchAll();
+ for iRow, oRow in enumerate(aaoRows):
+ oNew = SchedGroupDataEx().initFromDbRow(oRow);
+ iEntry = findEntry(oNew.tsEffective, iEntry);
+ self._oDb.dprint('iRow=%s iEntry=%s' % (iRow, iEntry));
+ if iEntry < 0:
+ break;
+ oEntry = aoEntries[iEntry];
+ aoChanges = oEntry.aoChanges;
+ oEntry.oNewRaw = oNew;
+ if iRow + 1 < len(aaoRows):
+ oOld = SchedGroupDataEx().initFromDbRow(aaoRows[iRow + 1]);
+ self._oDb.dprint('oOld=%s' % (oOld,));
+ for sAttr in oNew.getDataAttributes():
+ if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+ oOldAttr = getattr(oOld, sAttr);
+ oNewAttr = getattr(oNew, sAttr);
+ if oOldAttr != oNewAttr:
+ aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+ else:
+ self._oDb.dprint('New');
+
+ #
+ # ...
+ #
+
+ # FInally
+ UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries);
+ return (aoEntries, fMore);
+
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """Add Scheduling Group record"""
+
+ #
+ # Validate.
+ #
+ dDataErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dDataErrors:
+ raise TMInvalidData('Invalid data passed to addEntry: %s' % (dDataErrors,));
+ if self.exists(oData.sName):
+ raise TMRowAlreadyExists('Scheduling group "%s" already exists.' % (oData.sName,));
+
+ #
+ # Add it.
+ #
+ self._oDb.execute('INSERT INTO SchedGroups (\n'
+ ' uidAuthor,\n'
+ ' sName,\n'
+ ' sDescription,\n'
+ ' fEnabled,\n'
+ ' enmScheduler,\n'
+ ' idBuildSrc,\n'
+ ' idBuildSrcTestSuite,\n'
+ ' sComment)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\n'
+ 'RETURNING idSchedGroup\n'
+ , ( uidAuthor,
+ oData.sName,
+ oData.sDescription,
+ oData.fEnabled,
+ oData.enmScheduler,
+ oData.idBuildSrc,
+ oData.idBuildSrcTestSuite,
+ oData.sComment ));
+ idSchedGroup = self._oDb.fetchOne()[0];
+ oData.idSchedGroup = idSchedGroup;
+
+ for oBoxInGrp in oData.aoTestBoxes:
+ oBoxInGrp.idSchedGroup = idSchedGroup;
+ self._addSchedGroupTestBox(uidAuthor, oBoxInGrp);
+
+ for oMember in oData.aoMembers:
+ oMember.idSchedGroup = idSchedGroup;
+ self._addSchedGroupMember(uidAuthor, oMember);
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """Edit Scheduling Group record"""
+
+ #
+ # Validate input and retrieve the old data.
+ #
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('editEntry got invalid data: %s' % (dErrors,));
+ self._assertUnique(oData.sName, oData.idSchedGroup);
+ oOldData = SchedGroupDataEx().initFromDbWithId(self._oDb, oData.idSchedGroup);
+
+ #
+ # Make the changes.
+ #
+ if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', 'aoMembers', 'aoTestBoxes',
+ 'oBuildSrc', 'oBuildSrcValidationKit', ]):
+ self._historizeEntry(oData.idSchedGroup);
+ self._readdEntry(uidAuthor, oData);
+
+ # Remove groups.
+ for oOld in oOldData.aoMembers:
+ fRemove = True;
+ for oNew in oData.aoMembers:
+ if oNew.idTestGroup == oOld.idTestGroup:
+ fRemove = False;
+ break;
+ if fRemove:
+ self._removeSchedGroupMember(uidAuthor, oOld);
+
+ # Add / modify groups.
+ for oMember in oData.aoMembers:
+ oOldMember = None;
+ for oOld in oOldData.aoMembers:
+ if oOld.idTestGroup == oMember.idTestGroup:
+ oOldMember = oOld;
+ break;
+
+ oMember.idSchedGroup = oData.idSchedGroup;
+ if oOldMember is None:
+ self._addSchedGroupMember(uidAuthor, oMember);
+ elif not oMember.isEqualEx(oOldMember, ['tsEffective', 'tsExpire', 'uidAuthor', 'oTestGroup']):
+ self._historizeSchedGroupMember(oMember);
+ self._addSchedGroupMember(uidAuthor, oMember);
+
+ # Remove testboxes.
+ for oOld in oOldData.aoTestBoxes:
+ fRemove = True;
+ for oNew in oData.aoTestBoxes:
+ if oNew.idTestBox == oOld.idTestBox:
+ fRemove = False;
+ break;
+ if fRemove:
+ self._removeSchedGroupTestBox(uidAuthor, oOld);
+
+ # Add / modify testboxes.
+ for oBoxInGrp in oData.aoTestBoxes:
+ oOldBoxInGrp = None;
+ for oOld in oOldData.aoTestBoxes:
+ if oOld.idTestBox == oBoxInGrp.idTestBox:
+ oOldBoxInGrp = oOld;
+ break;
+
+ oBoxInGrp.idSchedGroup = oData.idSchedGroup;
+ if oOldBoxInGrp is None:
+ self._addSchedGroupTestBox(uidAuthor, oBoxInGrp);
+ elif not oBoxInGrp.isEqualEx(oOldBoxInGrp, ['tsEffective', 'tsExpire', 'uidAuthor', 'oTestBox']):
+ self._historizeSchedGroupTestBox(oBoxInGrp);
+ self._addSchedGroupTestBox(uidAuthor, oBoxInGrp);
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def removeEntry(self, uidAuthor, idSchedGroup, fCascade = False, fCommit = False):
+ """
+ Deletes a scheduling group.
+ """
+ _ = fCascade;
+
+ #
+ # Input validation and retrival of current data.
+ #
+ if idSchedGroup == 1:
+ raise TMRowInUse('Cannot remove the default scheduling group (id 1).');
+ oData = SchedGroupDataEx().initFromDbWithId(self._oDb, idSchedGroup);
+
+ #
+ # Remove the test box member records.
+ #
+ for oBoxInGrp in oData.aoTestBoxes:
+ self._removeSchedGroupTestBox(uidAuthor, oBoxInGrp);
+ self._oDb.execute('UPDATE TestBoxesInSchedGroups\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idSchedGroup,));
+
+ #
+ # Remove the test group member records.
+ #
+ for oMember in oData.aoMembers:
+ self._removeSchedGroupMember(uidAuthor, oMember);
+ self._oDb.execute('UPDATE SchedGroupMembers\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idSchedGroup,));
+
+ #
+ # Now the SchedGroups entry.
+ #
+ (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
+ if oData.tsEffective not in (tsCur, tsCurMinusOne):
+ self._historizeEntry(idSchedGroup, tsCurMinusOne);
+ self._readdEntry(uidAuthor, oData, tsCurMinusOne);
+ self._historizeEntry(idSchedGroup);
+ self._oDb.execute('UPDATE SchedGroups\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idSchedGroup,))
+
+ self._oDb.maybeCommit(fCommit)
+ return True;
+
+
+ def cachedLookup(self, idSchedGroup):
+ """
+ Looks up the most recent SchedGroupData object for idSchedGroup
+ via an object cache.
+
+ Returns a shared SchedGroupData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('SchedGroup');
+
+ oEntry = self.dCache.get(idSchedGroup, None);
+ if oEntry is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idSchedGroup, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE idSchedGroup = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idSchedGroup, ));
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idSchedGroup));
+
+ if self._oDb.getRowCount() == 1:
+ oEntry = SchedGroupData().initFromDbRow(self._oDb.fetchOne());
+ self.dCache[idSchedGroup] = oEntry;
+ return oEntry;
+
+
+ #
+ # Other methods.
+ #
+
+ def fetchOrderedByName(self, tsNow = None):
+ """
+ Return list of objects of type SchedGroups ordered by name.
+ May raise exception on database error.
+ """
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sName ASC\n');
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY sName ASC\n'
+ , (tsNow, tsNow,));
+ aoRet = []
+ for _ in range(self._oDb.getRowCount()):
+ aoRet.append(SchedGroupData().initFromDbRow(self._oDb.fetchOne()));
+ return aoRet;
+
+
+ def getAll(self, tsEffective = None):
+ """
+ Gets the list of all scheduling groups.
+ Returns an array of SchedGroupData instances.
+ """
+ if tsEffective is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n');
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ , (tsEffective, tsEffective));
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(SchedGroupData().initFromDbRow(aoRow));
+ return aoRet;
+
+ def getSchedGroupsForCombo(self, tsEffective = None):
+ """
+ Gets the list of active scheduling groups for a combo box.
+ Returns an array of (value [idSchedGroup], drop-down-name [sName],
+ hover-text [sDescription]) tuples.
+ """
+ if tsEffective is None:
+ self._oDb.execute('SELECT idSchedGroup, sName, sDescription\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sName');
+ else:
+ self._oDb.execute('SELECT idSchedGroup, sName, sDescription\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY sName'
+ , (tsEffective, tsEffective));
+ return self._oDb.fetchAll();
+
+
+ def getMembers(self, idSchedGroup, tsEffective = None):
+ """
+ Gets the scheduling groups members for the given scheduling group.
+
+ Returns an array of SchedGroupMemberDataEx instances (sorted by
+ priority (descending) and idTestGroup). May raise exception DB error.
+ """
+
+ if tsEffective is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroupMembers, TestGroups\n'
+ 'WHERE SchedGroupMembers.idSchedGroup = %s\n'
+ ' AND SchedGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestGroups.idTestGroup = SchedGroupMembers.idTestGroup\n'
+ ' AND TestGroups.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY SchedGroupMembers.iSchedPriority DESC, SchedGroupMembers.idTestGroup\n'
+ , (idSchedGroup,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroupMembers, TestGroups\n'
+ 'WHERE SchedGroupMembers.idSchedGroup = %s\n'
+ ' AND SchedGroupMembers.tsExpire < %s\n'
+ ' AND SchedGroupMembers.tsEffective >= %s\n'
+ ' AND TestGroups.idTestGroup = SchedGroupMembers.idTestGroup\n'
+ ' AND TestGroups.tsExpire < %s\n'
+ ' AND TestGroups.tsEffective >= %s\n'
+ 'ORDER BY SchedGroupMembers.iSchedPriority DESC, SchedGroupMembers.idTestGroup\n'
+ , (idSchedGroup, tsEffective, tsEffective, tsEffective, tsEffective, ));
+ aaoRows = self._oDb.fetchAll();
+ aoRet = [];
+ for aoRow in aaoRows:
+ aoRet.append(SchedGroupMemberDataEx().initFromDbRow(aoRow));
+ return aoRet;
+
+ def getTestCasesForGroup(self, idSchedGroup, cMax = None):
+ """
+ Gets the enabled testcases w/ testgroup+priority for the given scheduling group.
+
+ Returns an array of TestCaseData instances (ordered by group id, descending
+ testcase priority, and testcase IDs) with an extra iSchedPriority member.
+ May raise exception on DB error or if the result exceeds cMax.
+ """
+
+ self._oDb.execute('SELECT TestGroupMembers.idTestGroup, TestGroupMembers.iSchedPriority, TestCases.*\n'
+ 'FROM SchedGroupMembers, TestGroups, TestGroupMembers, TestCases\n'
+ 'WHERE SchedGroupMembers.idSchedGroup = %s\n'
+ ' AND SchedGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestGroups.idTestGroup = SchedGroupMembers.idTestGroup\n'
+ ' AND TestGroups.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestGroupMembers.idTestGroup = TestGroups.idTestGroup\n'
+ ' AND TestGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestCases.idTestCase = TestGroupMembers.idTestCase\n'
+ ' AND TestCases.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestCases.fEnabled = TRUE\n'
+ 'ORDER BY TestGroupMembers.idTestGroup, TestGroupMembers.iSchedPriority DESC, TestCases.idTestCase\n'
+ , (idSchedGroup,));
+
+ if cMax is not None and self._oDb.getRowCount() > cMax:
+ raise TMExceptionBase('Too many testcases for scheduling group %s: %s, max %s'
+ % (idSchedGroup, cMax, self._oDb.getRowCount(),));
+
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ oTestCase = TestCaseData().initFromDbRow(aoRow[2:]);
+ oTestCase.idTestGroup = aoRow[0];
+ oTestCase.iSchedPriority = aoRow[1];
+ aoRet.append(oTestCase);
+ return aoRet;
+
+ def getTestCaseArgsForGroup(self, idSchedGroup, cMax = None):
+ """
+ Gets the testcase argument variation w/ testgroup+priority for the given scheduling group.
+
+ Returns an array TestCaseArgsData instance (sorted by group and
+ variation id) with an extra iSchedPriority member.
+ May raise exception on DB error or if the result exceeds cMax.
+ """
+
+ self._oDb.execute('SELECT TestGroupMembers.idTestGroup, TestGroupMembers.iSchedPriority, TestCaseArgs.*\n'
+ 'FROM SchedGroupMembers, TestGroups, TestGroupMembers, TestCaseArgs, TestCases\n'
+ 'WHERE SchedGroupMembers.idSchedGroup = %s\n'
+ ' AND SchedGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestGroups.idTestGroup = SchedGroupMembers.idTestGroup\n'
+ ' AND TestGroups.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestGroupMembers.idTestGroup = TestGroups.idTestGroup\n'
+ ' AND TestGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestCaseArgs.idTestCase = TestGroupMembers.idTestCase\n'
+ ' AND TestCaseArgs.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND ( TestGroupMembers.aidTestCaseArgs is NULL\n'
+ ' OR TestCaseArgs.idTestCaseArgs = ANY(TestGroupMembers.aidTestCaseArgs) )\n'
+ ' AND TestCases.idTestCase = TestCaseArgs.idTestCase\n'
+ ' AND TestCases.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestCases.fEnabled = TRUE\n'
+ 'ORDER BY TestGroupMembers.idTestGroup, TestGroupMembers.idTestCase, TestCaseArgs.idTestCaseArgs\n'
+ , (idSchedGroup,));
+
+ if cMax is not None and self._oDb.getRowCount() > cMax:
+ raise TMExceptionBase('Too many argument variations for scheduling group %s: %s, max %s'
+ % (idSchedGroup, cMax, self._oDb.getRowCount(),));
+
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ oVariation = TestCaseArgsData().initFromDbRow(aoRow[2:]);
+ oVariation.idTestGroup = aoRow[0];
+ oVariation.iSchedPriority = aoRow[1];
+ aoRet.append(oVariation);
+ return aoRet;
+
+ def exists(self, sName):
+ """Checks if a group with the given name exists."""
+ self._oDb.execute('SELECT idSchedGroup\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND sName = %s\n'
+ 'LIMIT 1\n'
+ , (sName,));
+ return self._oDb.getRowCount() > 0;
+
+ def getById(self, idSchedGroup):
+ """Get Scheduling Group data by idSchedGroup"""
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire = \'infinity\'::timestamp\n'
+ ' AND idSchedGroup = %s;', (idSchedGroup,))
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise self._oDb.integrityException(
+ 'Found more than one scheduling groups with the same credentials. Database structure is corrupted.')
+ try:
+ return SchedGroupData().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+
+ #
+ # Internal helpers.
+ #
+
+ def _assertUnique(self, sName, idSchedGroupIgnore = None):
+ """
+ Checks that the scheduling group name is unique.
+ Raises exception if the name is already in use.
+ """
+ if idSchedGroupIgnore is None:
+ self._oDb.execute('SELECT idSchedGroup\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND sName = %s\n'
+ , ( sName, ) );
+ else:
+ self._oDb.execute('SELECT idSchedGroup\n'
+ 'FROM SchedGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND sName = %s\n'
+ ' AND idSchedGroup <> %s\n'
+ , ( sName, idSchedGroupIgnore, ) );
+ if self._oDb.getRowCount() > 0:
+ raise TMRowInUse('Scheduling group name (%s) is already in use.' % (sName,));
+ return True;
+
+ def _readdEntry(self, uidAuthor, oData, tsEffective = None):
+ """
+ Re-adds the SchedGroups entry. Used by editEntry and removeEntry.
+ """
+ if tsEffective is None:
+ tsEffective = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('INSERT INTO SchedGroups (\n'
+ ' uidAuthor,\n'
+ ' tsEffective,\n'
+ ' idSchedGroup,\n'
+ ' sName,\n'
+ ' sDescription,\n'
+ ' fEnabled,\n'
+ ' enmScheduler,\n'
+ ' idBuildSrc,\n'
+ ' idBuildSrcTestSuite,\n'
+ ' sComment )\n'
+ 'VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s )\n'
+ , ( uidAuthor,
+ tsEffective,
+ oData.idSchedGroup,
+ oData.sName,
+ oData.sDescription,
+ oData.fEnabled,
+ oData.enmScheduler,
+ oData.idBuildSrc,
+ oData.idBuildSrcTestSuite,
+ oData.sComment, ));
+ return True;
+
+ def _historizeEntry(self, idSchedGroup, tsExpire = None):
+ """
+ Historizes the current entry for the given scheduling group.
+ """
+ if tsExpire is None:
+ tsExpire = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('UPDATE SchedGroups\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( tsExpire, idSchedGroup, ));
+ return True;
+
+ def _addSchedGroupMember(self, uidAuthor, oMember, tsEffective = None):
+ """
+ addEntry worker for adding a scheduling group member.
+ """
+ if tsEffective is None:
+ tsEffective = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('INSERT INTO SchedGroupMembers(\n'
+ ' idSchedGroup,\n'
+ ' idTestGroup,\n'
+ ' tsEffective,\n'
+ ' uidAuthor,\n'
+ ' iSchedPriority,\n'
+ ' bmHourlySchedule,\n'
+ ' idTestGroupPreReq)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s)\n'
+ , ( oMember.idSchedGroup,
+ oMember.idTestGroup,
+ tsEffective,
+ uidAuthor,
+ oMember.iSchedPriority,
+ oMember.bmHourlySchedule,
+ oMember.idTestGroupPreReq, ));
+ return True;
+
+ def _removeSchedGroupMember(self, uidAuthor, oMember):
+ """
+ Removes a scheduling group member.
+ """
+
+ # Try record who removed it by adding an dummy entry that expires immediately.
+ (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
+ if oMember.tsEffective not in (tsCur, tsCurMinusOne):
+ self._historizeSchedGroupMember(oMember, tsCurMinusOne);
+ self._addSchedGroupMember(uidAuthor, oMember, tsCurMinusOne); # lazy bird.
+ self._historizeSchedGroupMember(oMember);
+ else:
+ self._historizeSchedGroupMember(oMember);
+ return True;
+
+ def _historizeSchedGroupMember(self, oMember, tsExpire = None):
+ """
+ Historizes the current entry for the given scheduling group.
+ """
+ if tsExpire is None:
+ tsExpire = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('UPDATE SchedGroupMembers\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND idTestGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( tsExpire, oMember.idSchedGroup, oMember.idTestGroup, ));
+ return True;
+
+ #
+ def _addSchedGroupTestBox(self, uidAuthor, oBoxInGroup, tsEffective = None):
+ """
+ addEntry worker for adding a test box to a scheduling group.
+ """
+ if tsEffective is None:
+ tsEffective = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('INSERT INTO TestBoxesInSchedGroups(\n'
+ ' idSchedGroup,\n'
+ ' idTestBox,\n'
+ ' tsEffective,\n'
+ ' uidAuthor,\n'
+ ' iSchedPriority)\n'
+ 'VALUES (%s, %s, %s, %s, %s)\n'
+ , ( oBoxInGroup.idSchedGroup,
+ oBoxInGroup.idTestBox,
+ tsEffective,
+ uidAuthor,
+ oBoxInGroup.iSchedPriority, ));
+ return True;
+
+ def _removeSchedGroupTestBox(self, uidAuthor, oBoxInGroup):
+ """
+ Removes a testbox from a scheduling group.
+ """
+
+ # Try record who removed it by adding an dummy entry that expires immediately.
+ (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
+ if oBoxInGroup.tsEffective not in (tsCur, tsCurMinusOne):
+ self._historizeSchedGroupTestBox(oBoxInGroup, tsCurMinusOne);
+ self._addSchedGroupTestBox(uidAuthor, oBoxInGroup, tsCurMinusOne); # lazy bird.
+ self._historizeSchedGroupTestBox(oBoxInGroup);
+ else:
+ self._historizeSchedGroupTestBox(oBoxInGroup);
+ return True;
+
+ def _historizeSchedGroupTestBox(self, oBoxInGroup, tsExpire = None):
+ """
+ Historizes the current entry for the given scheduling group.
+ """
+ if tsExpire is None:
+ tsExpire = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('UPDATE TestBoxesInSchedGroups\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND idTestBox = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( tsExpire, oBoxInGroup.idSchedGroup, oBoxInGroup.idTestBox, ));
+ return True;
+
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class SchedGroupMemberDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [SchedGroupMemberData(),];
+
+class SchedGroupDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [SchedGroupData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/schedqueue.py b/src/VBox/ValidationKit/testmanager/core/schedqueue.py
new file mode 100755
index 00000000..f49c4ad3
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/schedqueue.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# "$Id: schedqueue.py $"
+
+"""
+Test Manager - Test Case Queue.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+## Standard python imports.
+#import unittest
+
+from testmanager.core.base import ModelDataBase, ModelLogicBase, TMExceptionBase #, ModelDataBaseTestCase
+
+
+class SchedQueueEntry(ModelDataBase):
+ """
+ SchedQueue listing entry
+
+ Note! This could be turned into a SchedQueueDataEx class if we start
+ fetching all the fields from the scheduing queue.
+ """
+
+ def __init__(self):
+ ModelDataBase.__init__(self)
+
+ self.idItem = None
+ self.tsLastScheduled = None
+ self.sSchedGroup = None
+ self.sTestGroup = None
+ self.sTestCase = None
+ self.fUpToDate = None
+ self.iPerSchedGroupRowNumber = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the object from a SchedQueueLogic::fetchForListing select.
+ Returns self. Raises exception if aoRow is None.
+ """
+ if aoRow is None:
+ raise TMExceptionBase('TestCaseQueue row not found.')
+
+ self.idItem = aoRow[0]
+ self.tsLastScheduled = aoRow[1]
+ self.sSchedGroup = aoRow[2]
+ self.sTestGroup = aoRow[3]
+ self.sTestCase = aoRow[4]
+ self.fUpToDate = aoRow[5]
+ self.iPerSchedGroupRowNumber = aoRow[6];
+ return self
+
+
+class SchedQueueLogic(ModelLogicBase):
+ """
+ SchedQueues logic.
+ """
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches SchedQueues entries.
+
+ Returns an array (list) of SchedQueueEntry items, empty list if none.
+ Raises exception on error.
+ """
+ _, _ = tsNow, aiSortColumns
+ self._oDb.execute('''
+SELECT SchedQueues.idItem,
+ SchedQueues.tsLastScheduled,
+ SchedGroups.sName,
+ TestGroups.sName,
+ TestCases.sName,
+ SchedGroups.tsExpire = 'infinity'::TIMESTAMP
+ AND TestGroups.tsExpire = 'infinity'::TIMESTAMP
+ AND TestGroups.tsExpire = 'infinity'::TIMESTAMP
+ AND TestCaseArgs.tsExpire = 'infinity'::TIMESTAMP
+ AND TestCases.tsExpire = 'infinity'::TIMESTAMP AS fUpToDate,
+ ROW_NUMBER() OVER (PARTITION BY SchedQueues.idSchedGroup
+ ORDER BY SchedQueues.tsLastScheduled,
+ SchedQueues.idItem) AS iPerSchedGroupRowNumber
+FROM SchedQueues
+ INNER JOIN SchedGroups
+ ON SchedGroups.idSchedGroup = SchedQueues.idSchedGroup
+ AND SchedGroups.tsExpire > SchedQueues.tsConfig
+ AND SchedGroups.tsEffective <= SchedQueues.tsConfig
+ INNER JOIN TestGroups
+ ON TestGroups.idTestGroup = SchedQueues.idTestGroup
+ AND TestGroups.tsExpire > SchedQueues.tsConfig
+ AND TestGroups.tsEffective <= SchedQueues.tsConfig
+ INNER JOIN TestCaseArgs
+ ON TestCaseArgs.idGenTestCaseArgs = SchedQueues.idGenTestCaseArgs
+ INNER JOIN TestCases
+ ON TestCases.idTestCase = TestCaseArgs.idTestCase
+ AND TestCases.tsExpire > SchedQueues.tsConfig
+ AND TestCases.tsEffective <= SchedQueues.tsConfig
+ORDER BY iPerSchedGroupRowNumber,
+ SchedGroups.sName DESC
+LIMIT %s OFFSET %s''' % (cMaxRows, iStart,))
+ aoRows = []
+ for _ in range(self._oDb.getRowCount()):
+ aoRows.append(SchedQueueEntry().initFromDbRow(self._oDb.fetchOne()))
+ return aoRows
+
+#
+# Unit testing.
+#
+
+## @todo SchedQueueEntry isn't a typical ModelDataBase child (not fetching all
+## fields; is an extended data class mixing data from multiple tables), so
+## this won't work yet.
+#
+## pylint: disable=missing-docstring
+#class TestCaseQueueDataTestCase(ModelDataBaseTestCase):
+# def setUp(self):
+# self.aoSamples = [SchedQueueEntry(),]
+#
+#
+#if __name__ == '__main__':
+# unittest.main()
+# # not reached.
+#
diff --git a/src/VBox/ValidationKit/testmanager/core/schedulerbase.py b/src/VBox/ValidationKit/testmanager/core/schedulerbase.py
new file mode 100755
index 00000000..c28b43cf
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/schedulerbase.py
@@ -0,0 +1,1570 @@
+# -*- coding: utf-8 -*-
+# $Id: schedulerbase.py $
+# pylint: disable=too-many-lines
+
+
+"""
+Test Manager - Base class and utilities for the schedulers.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import sys;
+import unittest;
+
+# Validation Kit imports.
+from common import utils, constants;
+from testmanager import config;
+from testmanager.core.build import BuildDataEx, BuildLogic;
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, TMExceptionBase;
+from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+from testmanager.core.globalresource import GlobalResourceLogic;
+from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic;
+from testmanager.core.systemlog import SystemLogData, SystemLogLogic;
+from testmanager.core.testbox import TestBoxData, TestBoxDataEx;
+from testmanager.core.testboxstatus import TestBoxStatusData, TestBoxStatusLogic;
+from testmanager.core.testcase import TestCaseLogic;
+from testmanager.core.testcaseargs import TestCaseArgsDataEx, TestCaseArgsLogic;
+from testmanager.core.testset import TestSetData, TestSetLogic;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ xrange = range; # pylint: disable=redefined-builtin,invalid-name
+
+
+
+class ReCreateQueueData(object):
+ """
+ Data object for recreating a scheduling queue.
+
+ It's mostly a storage object, but has a few data checking operation
+ associated with it.
+ """
+
+ def __init__(self, oDb, idSchedGroup):
+ #
+ # Load data from the database.
+ #
+ oSchedGroupLogic = SchedGroupLogic(oDb);
+ self.oSchedGroup = oSchedGroupLogic.cachedLookup(idSchedGroup);
+
+ # Will extend the entries with aoTestCases and dTestCases members
+ # further down (SchedGroupMemberDataEx). checkForGroupDepCycles
+ # will add aidTestGroupPreReqs.
+ self.aoTestGroups = oSchedGroupLogic.getMembers(idSchedGroup);
+
+ # aoTestCases entries are TestCaseData instance with iSchedPriority
+ # and idTestGroup added for our purposes.
+ # We will add oTestGroup and aoArgsVariations members to each further down.
+ self.aoTestCases = oSchedGroupLogic.getTestCasesForGroup(idSchedGroup, cMax = 4096);
+
+ # Load dependencies.
+ oTestCaseLogic = TestCaseLogic(oDb)
+ for oTestCase in self.aoTestCases:
+ oTestCase.aidPreReqs = oTestCaseLogic.getTestCasePreReqIds(oTestCase.idTestCase, cMax = 4096);
+
+ # aoTestCases entries are TestCaseArgsData instance with iSchedPriority
+ # and idTestGroup added for our purposes.
+ # We will add oTestGroup and oTestCase members to each further down.
+ self.aoArgsVariations = oSchedGroupLogic.getTestCaseArgsForGroup(idSchedGroup, cMax = 65536);
+
+ #
+ # Generate global lookups.
+ #
+
+ # Generate a testcase lookup dictionary for use when working on
+ # argument variations.
+ self.dTestCases = {};
+ for oTestCase in self.aoTestCases:
+ self.dTestCases[oTestCase.idTestCase] = oTestCase;
+ assert len(self.dTestCases) <= len(self.aoTestCases); # Note! Can be shorter!
+
+ # Generate a testgroup lookup dictionary.
+ self.dTestGroups = {};
+ for oTestGroup in self.aoTestGroups:
+ self.dTestGroups[oTestGroup.idTestGroup] = oTestGroup;
+ assert len(self.dTestGroups) == len(self.aoTestGroups);
+
+ #
+ # Associate extra members with the base data.
+ #
+ if self.aoTestGroups:
+ # Prep the test groups.
+ for oTestGroup in self.aoTestGroups:
+ oTestGroup.aoTestCases = [];
+ oTestGroup.dTestCases = {};
+
+ # Link testcases to their group, both directions. Prep testcases for
+ # argument varation association.
+ oTestGroup = self.aoTestGroups[0];
+ for oTestCase in self.aoTestCases:
+ if oTestGroup.idTestGroup != oTestCase.idTestGroup:
+ oTestGroup = self.dTestGroups[oTestCase.idTestGroup];
+
+ assert oTestCase.idTestCase not in oTestGroup.dTestCases;
+ oTestGroup.dTestCases[oTestCase.idTestCase] = oTestCase;
+ oTestGroup.aoTestCases.append(oTestCase);
+ oTestCase.oTestGroup = oTestGroup;
+ oTestCase.aoArgsVariations = [];
+
+ # Associate testcase argument variations with their testcases (group)
+ # in both directions.
+ oTestGroup = self.aoTestGroups[0];
+ oTestCase = self.aoTestCases[0] if self.aoTestCases else None;
+ for oArgVariation in self.aoArgsVariations:
+ if oTestGroup.idTestGroup != oArgVariation.idTestGroup:
+ oTestGroup = self.dTestGroups[oArgVariation.idTestGroup];
+ if oTestCase.idTestCase != oArgVariation.idTestCase or oTestCase.idTestGroup != oArgVariation.idTestGroup:
+ oTestCase = oTestGroup.dTestCases[oArgVariation.idTestCase];
+
+ oTestCase.aoArgsVariations.append(oArgVariation);
+ oArgVariation.oTestCase = oTestCase;
+ oArgVariation.oTestGroup = oTestGroup;
+
+ else:
+ assert not self.aoTestCases;
+ assert not self.aoArgsVariations;
+ # done.
+
+ @staticmethod
+ def _addPreReqError(aoErrors, aidChain, oObj, sMsg):
+ """ Returns a chain of IDs error entry. """
+
+ sMsg += ' Dependency chain: %s' % (aidChain[0],);
+ for i in range(1, len(aidChain)):
+ sMsg += ' -> %s' % (aidChain[i],);
+
+ aoErrors.append([sMsg, oObj]);
+ return aoErrors;
+
+ def checkForGroupDepCycles(self):
+ """
+ Checks for testgroup depencency cycles and any missing testgroup
+ dependencies.
+ Returns array of errors (see SchedulderBase.recreateQueue()).
+ """
+ aoErrors = [];
+ for oTestGroup in self.aoTestGroups:
+ idPreReq = oTestGroup.idTestGroupPreReq;
+ if idPreReq is None:
+ oTestGroup.aidTestGroupPreReqs = [];
+ continue;
+
+ aidChain = [oTestGroup.idTestGroup,];
+ while idPreReq is not None:
+ aidChain.append(idPreReq);
+ if len(aidChain) >= 10:
+ self._addPreReqError(aoErrors, aidChain, oTestGroup,
+ 'TestGroup #%s prerequisite chain is too long!'
+ % (oTestGroup.idTestGroup,));
+ break;
+
+ oDep = self.dTestGroups.get(idPreReq, None);
+ if oDep is None:
+ self._addPreReqError(aoErrors, aidChain, oTestGroup,
+ 'TestGroup #%s prerequisite #%s is not in the scheduling group!'
+ % (oTestGroup.idTestGroup, idPreReq,));
+ break;
+
+ idPreReq = oDep.idTestGroupPreReq;
+ oTestGroup.aidTestGroupPreReqs = aidChain[1:];
+
+ return aoErrors;
+
+
+ def checkForMissingTestCaseDeps(self):
+ """
+ Checks that testcase dependencies stays within bounds. We do not allow
+ dependencies outside a testgroup, no dependency cycles or even remotely
+ long dependency chains.
+
+ Returns array of errors (see SchedulderBase.recreateQueue()).
+ """
+ aoErrors = [];
+ for oTestGroup in self.aoTestGroups:
+ for oTestCase in oTestGroup.aoTestCases:
+ if not oTestCase.aidPreReqs:
+ continue;
+
+ # Stupid recursion code using special stack(s).
+ aiIndexes = [[oTestCase, 0], ];
+ aidChain = [oTestCase.idTestGroup,];
+ while aiIndexes:
+ (oCur, i) = aiIndexes[-1];
+ if i >= len(oCur.aidPreReqs):
+ aiIndexes.pop();
+ aidChain.pop();
+ else:
+ aiIndexes[-1][1] = i + 1; # whatever happens, we'll advance on the current level.
+
+ idPreReq = oTestCase.aidPreReqs[i];
+ oDep = oTestGroup.dTestCases.get(idPreReq, None);
+ if oDep is None:
+ self._addPreReqError(aoErrors, aidChain, oTestCase,
+ 'TestCase #%s prerequisite #%s is not in the scheduling group!'
+ % (oTestCase.idTestCase, idPreReq));
+ elif idPreReq in aidChain:
+ self._addPreReqError(aoErrors, aidChain, oTestCase,
+ 'TestCase #%s prerequisite #%s creates a cycle!'
+ % (oTestCase.idTestCase, idPreReq));
+ elif not oDep.aiPreReqs:
+ pass;
+ elif len(aidChain) >= 10:
+ self._addPreReqError(aoErrors, aidChain, oTestCase,
+ 'TestCase #%s prerequisite chain is too long!' % (oTestCase.idTestCase,));
+ else:
+ aiIndexes.append([oDep, 0]);
+ aidChain.append(idPreReq);
+
+ return aoErrors;
+
+ def deepTestGroupSort(self):
+ """
+ Sorts the testgroups and their testcases by priority and dependencies.
+ Note! Don't call this before checking for dependency cycles!
+ """
+ if not self.aoTestGroups:
+ return;
+
+ #
+ # ASSUMES groups as well as testcases are sorted by priority by the
+ # database. So we only have to concern ourselves with the dependency
+ # sorting.
+ #
+ iGrpPrio = self.aoTestGroups[0].iSchedPriority;
+ for iTestGroup, oTestGroup in enumerate(self.aoTestGroups):
+ if oTestGroup.iSchedPriority > iGrpPrio:
+ raise TMExceptionBase('Incorrectly sorted testgroups returned by database: iTestGroup=%s prio=%s %s'
+ % ( iTestGroup, iGrpPrio,
+ ', '.join(['(%s: %s)' % (oCur.idTestGroup, oCur.iSchedPriority)
+ for oCur in self.aoTestGroups]), ) );
+ iGrpPrio = oTestGroup.iSchedPriority;
+
+ if oTestGroup.aoTestCases:
+ iTstPrio = oTestGroup.aoTestCases[0].iSchedPriority;
+ for iTestCase, oTestCase in enumerate(oTestGroup.aoTestCases):
+ if oTestCase.iSchedPriority > iTstPrio:
+ raise TMExceptionBase('Incorrectly sorted testcases returned by database: i=%s prio=%s idGrp=%s %s'
+ % ( iTestCase, iTstPrio, oTestGroup.idTestGroup,
+ ', '.join(['(%s: %s)' % (oCur.idTestCase, oCur.iSchedPriority)
+ for oCur in oTestGroup.aoTestCases]),));
+
+ #
+ # Sort the testgroups by dependencies.
+ #
+ i = 0;
+ while i < len(self.aoTestGroups):
+ oTestGroup = self.aoTestGroups[i];
+ if oTestGroup.idTestGroupPreReq is not None:
+ iPreReq = self.aoTestGroups.index(self.dTestGroups[oTestGroup.idTestGroupPreReq]);
+ if iPreReq > i:
+ # The prerequisite is after the current entry. Move the
+ # current entry so that it's following it's prereq entry.
+ self.aoTestGroups.insert(iPreReq + 1, oTestGroup);
+ self.aoTestGroups.pop(i);
+ continue;
+ assert iPreReq < i;
+ i += 1; # Advance.
+
+ #
+ # Sort the testcases by dependencies.
+ # Same algorithm as above, just more prerequisites.
+ #
+ for oTestGroup in self.aoTestGroups:
+ i = 0;
+ while i < len(oTestGroup.aoTestCases):
+ oTestCase = oTestGroup.aoTestCases[i];
+ if oTestCase.aidPreReqs:
+ for idPreReq in oTestCase.aidPreReqs:
+ iPreReq = oTestGroup.aoTestCases.index(oTestGroup.dTestCases[idPreReq]);
+ if iPreReq > i:
+ # The prerequisite is after the current entry. Move the
+ # current entry so that it's following it's prereq entry.
+ oTestGroup.aoTestGroups.insert(iPreReq + 1, oTestCase);
+ oTestGroup.aoTestGroups.pop(i);
+ i -= 1; # Don't advance.
+ break;
+ assert iPreReq < i;
+ i += 1; # Advance.
+
+
+
+class SchedQueueData(ModelDataBase):
+ """
+ Scheduling queue data item.
+ """
+
+ ksIdAttr = 'idSchedGroup';
+
+ ksParam_idSchedGroup = 'SchedQueueData_idSchedGroup';
+ ksParam_idItem = 'SchedQueueData_idItem';
+ ksParam_offQueue = 'SchedQueueData_offQueue';
+ ksParam_idGenTestCaseArgs = 'SchedQueueData_idGenTestCaseArgs';
+ ksParam_idTestGroup = 'SchedQueueData_idTestGroup';
+ ksParam_aidTestGroupPreReqs = 'SchedQueueData_aidTestGroupPreReqs';
+ ksParam_bmHourlySchedule = 'SchedQueueData_bmHourlySchedule';
+ ksParam_tsConfig = 'SchedQueueData_tsConfig';
+ ksParam_tsLastScheduled = 'SchedQueueData_tsLastScheduled';
+ ksParam_idTestSetGangLeader = 'SchedQueueData_idTestSetGangLeader';
+ ksParam_cMissingGangMembers = 'SchedQueueData_cMissingGangMembers';
+
+ kasAllowNullAttributes = [ 'idItem', 'offQueue', 'aidTestGroupPreReqs', 'bmHourlySchedule', 'idTestSetGangLeader',
+ 'tsConfig', 'tsLastScheduled' ];
+
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idSchedGroup = None;
+ self.idItem = None;
+ self.offQueue = None;
+ self.idGenTestCaseArgs = None;
+ self.idTestGroup = None;
+ self.aidTestGroupPreReqs = None;
+ self.bmHourlySchedule = None;
+ self.tsConfig = None;
+ self.tsLastScheduled = None;
+ self.idTestSetGangLeader = None;
+ self.cMissingGangMembers = 1;
+
+ def initFromValues(self, idSchedGroup, idGenTestCaseArgs, idTestGroup, aidTestGroupPreReqs, # pylint: disable=too-many-arguments
+ bmHourlySchedule, cMissingGangMembers,
+ idItem = None, offQueue = None, tsConfig = None, tsLastScheduled = None, idTestSetGangLeader = None):
+ """
+ Reinitialize with all attributes potentially given as inputs.
+ Return self.
+ """
+ self.idSchedGroup = idSchedGroup;
+ self.idItem = idItem;
+ self.offQueue = offQueue;
+ self.idGenTestCaseArgs = idGenTestCaseArgs;
+ self.idTestGroup = idTestGroup;
+ self.aidTestGroupPreReqs = aidTestGroupPreReqs;
+ self.bmHourlySchedule = bmHourlySchedule;
+ self.tsConfig = tsConfig;
+ self.tsLastScheduled = tsLastScheduled;
+ self.idTestSetGangLeader = idTestSetGangLeader;
+ self.cMissingGangMembers = cMissingGangMembers;
+ return self;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Initialize from database row (SELECT * FROM SchedQueues).
+ Returns self.
+ Raises exception if no row is specfied.
+ """
+ if aoRow is None:
+ raise TMExceptionBase('SchedQueueData not found.');
+
+ self.idSchedGroup = aoRow[0];
+ self.idItem = aoRow[1];
+ self.offQueue = aoRow[2];
+ self.idGenTestCaseArgs = aoRow[3];
+ self.idTestGroup = aoRow[4];
+ self.aidTestGroupPreReqs = aoRow[5];
+ self.bmHourlySchedule = aoRow[6];
+ self.tsConfig = aoRow[7];
+ self.tsLastScheduled = aoRow[8];
+ self.idTestSetGangLeader = aoRow[9];
+ self.cMissingGangMembers = aoRow[10];
+ return self;
+
+
+
+
+
+
+class SchedulerBase(object):
+ """
+ The scheduler base class.
+
+ The scheduler classes have two functions:
+ 1. Recreate the scheduling queue.
+ 2. Pick the next task from the queue.
+
+ The first is scheduler specific, the latter isn't.
+ """
+
+ class BuildCache(object):
+ """ Build cache. """
+
+ class BuildCacheIterator(object):
+ """ Build class iterator. """
+ def __init__(self, oCache):
+ self.oCache = oCache;
+ self.iCur = 0;
+
+ def __iter__(self):
+ """Returns self, required by the language."""
+ return self;
+
+ def __next__(self):
+ """Returns the next build, raises StopIteration when the end has been reached."""
+ while True:
+ if self.iCur >= len(self.oCache.aoEntries):
+ oEntry = self.oCache.fetchFromCursor();
+ if oEntry is None:
+ raise StopIteration;
+ else:
+ oEntry = self.oCache.aoEntries[self.iCur];
+ self.iCur += 1;
+ if not oEntry.fRemoved:
+ return oEntry;
+ return None; # not reached, but make pylint happy (for now).
+
+ def next(self):
+ """ For python 2.x. """
+ return self.__next__();
+
+ class BuildCacheEntry(object):
+ """ Build cache entry. """
+
+ def __init__(self, oBuild, fMaybeBlacklisted):
+ self.oBuild = oBuild;
+ self._fBlacklisted = None if fMaybeBlacklisted is True else False;
+ self.fRemoved = False;
+ self._dPreReqDecisions = {};
+
+ def remove(self):
+ """
+ Marks the cache entry as removed.
+ This doesn't actually remove it from the cache array, only marks
+ it as removed. It has no effect on open iterators.
+ """
+ self.fRemoved = True;
+
+ def getPreReqDecision(self, sPreReqSet):
+ """
+ Retrieves a cached prerequisite decision.
+ Returns boolean if found, None if not.
+ """
+ return self._dPreReqDecisions.get(sPreReqSet);
+
+ def setPreReqDecision(self, sPreReqSet, fDecision):
+ """
+ Caches a prerequistie decision.
+ """
+ self._dPreReqDecisions[sPreReqSet] = fDecision;
+ return fDecision;
+
+ def isBlacklisted(self, oDb):
+ """ Checks if the build is blacklisted. """
+ if self._fBlacklisted is None:
+ self._fBlacklisted = BuildLogic(oDb).isBuildBlacklisted(self.oBuild);
+ return self._fBlacklisted;
+
+
+ def __init__(self):
+ self.aoEntries = [];
+ self.oCursor = None;
+
+ def setupSource(self, oDb, idBuildSrc, sOs, sCpuArch, tsNow):
+ """ Configures the build cursor for the cache. """
+ if not self.aoEntries and self.oCursor is None:
+ oBuildSource = BuildSourceData().initFromDbWithId(oDb, idBuildSrc, tsNow);
+ self.oCursor = BuildSourceLogic(oDb).openBuildCursor(oBuildSource, sOs, sCpuArch, tsNow);
+ return True;
+
+ def __iter__(self):
+ """Return an iterator."""
+ return self.BuildCacheIterator(self);
+
+ def fetchFromCursor(self):
+ """ Fetches a build from the cursor and adds it to the cache."""
+ if self.oCursor is None:
+ return None;
+
+ try:
+ aoRow = self.oCursor.fetchOne();
+ except:
+ return None;
+ if aoRow is None:
+ return None;
+
+ oBuild = BuildDataEx().initFromDbRow(aoRow);
+ oEntry = self.BuildCacheEntry(oBuild, aoRow[-1]);
+ self.aoEntries.append(oEntry);
+ return oEntry;
+
+ def __init__(self, oDb, oSchedGrpData, iVerbosity = 0, tsSecStart = None):
+ self._oDb = oDb;
+ self._oSchedGrpData = oSchedGrpData;
+ self._iVerbosity = iVerbosity;
+ self._asMessages = [];
+ self._tsSecStart = tsSecStart if tsSecStart is not None else utils.timestampSecond();
+ self.oBuildCache = self.BuildCache();
+ self.dTestGroupMembers = {};
+
+ @staticmethod
+ def _instantiate(oDb, oSchedGrpData, iVerbosity = 0, tsSecStart = None):
+ """
+ Instantiate the scheduler specified by the scheduling group.
+ Returns scheduler child class instance. May raise exception if
+ the input is invalid.
+ """
+ if oSchedGrpData.enmScheduler == SchedGroupData.ksScheduler_BestEffortContinuousIntegration:
+ from testmanager.core.schedulerbeci import SchdulerBeci;
+ oScheduler = SchdulerBeci(oDb, oSchedGrpData, iVerbosity, tsSecStart);
+ else:
+ raise oDb.integrityException('Invalid scheduler "%s", idSchedGroup=%d' \
+ % (oSchedGrpData.enmScheduler, oSchedGrpData.idSchedGroup));
+ return oScheduler;
+
+
+ #
+ # Misc.
+ #
+
+ def msgDebug(self, sText):
+ """Debug printing."""
+ if self._iVerbosity > 1:
+ self._asMessages.append('debug:' + sText);
+ return None;
+
+ def msgInfo(self, sText):
+ """Info printing."""
+ if self._iVerbosity > 1:
+ self._asMessages.append('info: ' + sText);
+ return None;
+
+ def dprint(self, sMsg):
+ """Prints a debug message to the srv glue log (see config.py). """
+ if config.g_kfSrvGlueDebugScheduler:
+ self._oDb.dprint(sMsg);
+ return None;
+
+ def getElapsedSecs(self):
+ """ Returns the number of seconds this scheduling task has been running. """
+ tsSecNow = utils.timestampSecond();
+ if tsSecNow < self._tsSecStart: # paranoia
+ self._tsSecStart = tsSecNow;
+ return tsSecNow - self._tsSecStart;
+
+
+ #
+ # Create schedule.
+ #
+
+ def _recreateQueueCancelGatherings(self):
+ """
+ Cancels all pending gang gatherings on the current queue.
+ """
+ self._oDb.execute('SELECT idTestSetGangLeader\n'
+ 'FROM SchedQueues\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND idTestSetGangLeader is not NULL\n'
+ , (self._oSchedGrpData.idSchedGroup,));
+ if self._oDb.getRowCount() > 0:
+ oTBStatusLogic = TestBoxStatusLogic(self._oDb);
+ for aoRow in self._oDb.fetchAll():
+ idTestSetGangLeader = aoRow[0];
+ oTBStatusLogic.updateGangStatus(idTestSetGangLeader,
+ TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut,
+ fCommit = False);
+ return True;
+
+ def _recreateQueueItems(self, oData):
+ """
+ Returns an array of queue items (SchedQueueData).
+ Child classes must override this.
+ """
+ _ = oData;
+ return [];
+
+ def recreateQueueWorker(self):
+ """
+ Worker for recreateQueue.
+ """
+
+ #
+ # Collect the necessary data and validate it.
+ #
+ oData = ReCreateQueueData(self._oDb, self._oSchedGrpData.idSchedGroup);
+ aoErrors = oData.checkForGroupDepCycles();
+ aoErrors.extend(oData.checkForMissingTestCaseDeps());
+ if not aoErrors:
+ oData.deepTestGroupSort();
+
+ #
+ # The creation of the scheduling queue is done by the child class.
+ #
+ # We will try guess where in queue we're currently at and rotate
+ # the items such that we will resume execution in the approximately
+ # same position. The goal of the scheduler is to provide a 100%
+ # deterministic result so that if we regenerate the queue when there
+ # are no changes to the testcases, testgroups or scheduling groups
+ # involved, test execution will be unchanged (save for maybe just a
+ # little for gang gathering).
+ #
+ aoItems = [];
+ if not oData.oSchedGroup.fEnabled:
+ self.msgInfo('Disabled.');
+ elif not oData.aoArgsVariations:
+ self.msgInfo('Found no test case argument variations.');
+ else:
+ aoItems = self._recreateQueueItems(oData);
+ self.msgDebug('len(aoItems)=%s' % (len(aoItems),));
+ #for i in range(len(aoItems)):
+ # self.msgDebug('aoItems[%2d]=%s' % (i, aoItems[i]));
+ if aoItems:
+ self._oDb.execute('SELECT offQueue FROM SchedQueues WHERE idSchedGroup = %s ORDER BY idItem LIMIT 1'
+ , (self._oSchedGrpData.idSchedGroup,));
+ if self._oDb.getRowCount() > 0:
+ offQueue = self._oDb.fetchOne()[0];
+ self._oDb.execute('SELECT COUNT(*) FROM SchedQueues WHERE idSchedGroup = %s'
+ , (self._oSchedGrpData.idSchedGroup,));
+ cItems = self._oDb.fetchOne()[0];
+ offQueueNew = (offQueue * cItems) // len(aoItems);
+ if offQueueNew != 0:
+ aoItems = aoItems[offQueueNew:] + aoItems[:offQueueNew];
+
+ #
+ # Replace the scheduling queue.
+ # Care need to be take to first timeout/abort any gangs in the
+ # gathering state since these use the queue to set up the date.
+ #
+ self._recreateQueueCancelGatherings();
+ self._oDb.execute('DELETE FROM SchedQueues WHERE idSchedGroup = %s\n', (self._oSchedGrpData.idSchedGroup,));
+ if aoItems:
+ self._oDb.insertList('INSERT INTO SchedQueues (\n'
+ ' idSchedGroup,\n'
+ ' offQueue,\n'
+ ' idGenTestCaseArgs,\n'
+ ' idTestGroup,\n'
+ ' aidTestGroupPreReqs,\n'
+ ' bmHourlySchedule,\n'
+ ' cMissingGangMembers )\n',
+ aoItems, self._formatItemForInsert);
+ return (aoErrors, self._asMessages);
+
+ def _formatItemForInsert(self, oItem):
+ """
+ Used by recreateQueueWorker together with TMDatabaseConnect::insertList
+ """
+ return self._oDb.formatBindArgs('(%s,%s,%s,%s,%s,%s,%s)'
+ , ( oItem.idSchedGroup,
+ oItem.offQueue,
+ oItem.idGenTestCaseArgs,
+ oItem.idTestGroup,
+ oItem.aidTestGroupPreReqs if oItem.aidTestGroupPreReqs else None,
+ oItem.bmHourlySchedule,
+ oItem.cMissingGangMembers
+ ));
+
+ @staticmethod
+ def recreateQueue(oDb, uidAuthor, idSchedGroup, iVerbosity = 1):
+ """
+ (Re-)creates the scheduling queue for the given group.
+
+ Returns (asMessages, asMessages). On success the array with the error
+ will be empty, on failure it will contain (sError, oRelatedObject)
+ entries. The messages is for debugging and are simple strings.
+
+ Raises exception database error.
+ """
+
+ aoExtraMsgs = [];
+ if oDb.debugIsExplainEnabled():
+ aoExtraMsgs += ['Warning! Disabling SQL explain to avoid deadlocking against locked tables.'];
+ oDb.debugDisableExplain();
+
+ aoErrors = [];
+ asMessages = [];
+ try:
+ #
+ # To avoid concurrency issues (SchedQueues) and inconsistent data (*),
+ # we lock quite a few tables while doing this work. We access more
+ # data than scheduleNewTask so we lock some additional tables.
+ #
+ oDb.rollback();
+ oDb.begin();
+ oDb.execute('LOCK TABLE SchedGroups, SchedGroupMembers, TestGroups, TestGroupMembers IN SHARE MODE');
+ oDb.execute('LOCK TABLE TestBoxes, TestCaseArgs, TestCases IN SHARE MODE');
+ oDb.execute('LOCK TABLE TestBoxStatuses, SchedQueues IN EXCLUSIVE MODE');
+
+ #
+ # Instantiate the scheduler and call the worker function.
+ #
+ oSchedGrpData = SchedGroupData().initFromDbWithId(oDb, idSchedGroup);
+ oScheduler = SchedulerBase._instantiate(oDb, oSchedGrpData, iVerbosity);
+
+ (aoErrors, asMessages) = oScheduler.recreateQueueWorker();
+ if not aoErrors:
+ SystemLogLogic(oDb).addEntry(SystemLogData.ksEvent_SchedQueueRecreate,
+ 'User #%d recreated sched queue #%d.' % (uidAuthor, idSchedGroup,));
+ oDb.commit();
+ else:
+ oDb.rollback();
+
+ except:
+ oDb.rollback();
+ raise;
+
+ return (aoErrors, aoExtraMsgs + asMessages);
+
+
+ @staticmethod
+ def cleanUpOrphanedQueues(oDb):
+ """
+ Removes orphan scheduling queues from the SchedQueues table.
+
+ Queues becomes orphaned when the scheduling group they belongs to has been deleted.
+
+ Returns number of orphaned queues.
+ Raises exception database error.
+ """
+ cRet = 0;
+ try:
+ oDb.rollback();
+ oDb.begin();
+ oDb.execute('''
+SELECT SchedQueues.idSchedGroup
+FROM SchedQueues
+ LEFT OUTER JOIN SchedGroups
+ ON SchedGroups.idSchedGroup = SchedQueues.idSchedGroup
+ AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP
+WHERE SchedGroups.idSchedGroup is NULL
+GROUP BY SchedQueues.idSchedGroup''');
+ aaoOrphanRows = oDb.fetchAll();
+ cRet = len(aaoOrphanRows);
+ if cRet > 0:
+ oDb.execute('DELETE FROM SchedQueues WHERE idSchedGroup IN (%s)'
+ % (','.join([str(aoRow[0]) for aoRow in aaoOrphanRows]),));
+ oDb.commit();
+ except:
+ oDb.rollback();
+ raise;
+ return cRet;
+
+
+ #
+ # Schedule Task.
+ #
+
+ def _composeGangArguments(self, idTestSet):
+ """
+ Composes the gang specific testdriver arguments.
+ Returns command line string, including a leading space.
+ """
+
+ oTestSet = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ aoGangMembers = TestSetLogic(self._oDb).getGang(oTestSet.idTestSetGangLeader);
+
+ sArgs = ' --gang-member-no %s --gang-members %s' % (oTestSet.iGangMemberNo, len(aoGangMembers));
+ for i, _ in enumerate(aoGangMembers):
+ sArgs = ' --gang-ipv4-%s %s' % (i, aoGangMembers[i].ip); ## @todo IPv6
+
+ return sArgs;
+
+
+ def composeExecResponseWorker(self, idTestSet, oTestEx, oTestBox, oBuild, oValidationKitBuild, sBaseUrl):
+ """
+ Given all the bits of data, compose an EXEC command response to the testbox.
+ """
+ sScriptZips = oTestEx.oTestCase.sValidationKitZips;
+ if sScriptZips is None or sScriptZips.find('@VALIDATIONKIT_ZIP@') >= 0:
+ assert oValidationKitBuild;
+ if sScriptZips is None:
+ sScriptZips = oValidationKitBuild.sBinaries;
+ else:
+ sScriptZips = sScriptZips.replace('@VALIDATIONKIT_ZIP@', oValidationKitBuild.sBinaries);
+ sScriptZips = sScriptZips.replace('@DOWNLOAD_BASE_URL@', sBaseUrl + config.g_ksTmDownloadBaseUrlRel);
+
+ sCmdLine = oTestEx.oTestCase.sBaseCmd + ' ' + oTestEx.sArgs;
+ sCmdLine = sCmdLine.replace('@BUILD_BINARIES@', oBuild.sBinaries);
+ sCmdLine = sCmdLine.strip();
+ if oTestEx.cGangMembers > 1:
+ sCmdLine += ' ' + self._composeGangArguments(idTestSet);
+
+ cSecTimeout = oTestEx.cSecTimeout if oTestEx.cSecTimeout is not None else oTestEx.oTestCase.cSecTimeout;
+ cSecTimeout = cSecTimeout * oTestBox.pctScaleTimeout // 100;
+
+ dResponse = \
+ {
+ constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_EXEC,
+ constants.tbresp.EXEC_PARAM_RESULT_ID: idTestSet,
+ constants.tbresp.EXEC_PARAM_SCRIPT_ZIPS: sScriptZips,
+ constants.tbresp.EXEC_PARAM_SCRIPT_CMD_LINE: sCmdLine,
+ constants.tbresp.EXEC_PARAM_TIMEOUT: cSecTimeout,
+ };
+ return dResponse;
+
+ @staticmethod
+ def composeExecResponse(oDb, idTestSet, sBaseUrl, iVerbosity = 0):
+ """
+ Composes an EXEC response for a gang member (other than the last).
+ Returns a EXEC response or raises an exception (DB/input error).
+ """
+ #
+ # Gather the necessary data.
+ #
+ oTestSet = TestSetData().initFromDbWithId(oDb, idTestSet);
+ oTestBox = TestBoxData().initFromDbWithGenId(oDb, oTestSet.idGenTestBox);
+ oTestEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(oDb, oTestSet.idGenTestCaseArgs,
+ tsConfigEff = oTestSet.tsConfig,
+ tsRsrcEff = oTestSet.tsConfig);
+ oBuild = BuildDataEx().initFromDbWithId(oDb, oTestSet.idBuild);
+ oValidationKitBuild = None;
+ if oTestSet.idBuildTestSuite is not None:
+ oValidationKitBuild = BuildDataEx().initFromDbWithId(oDb, oTestSet.idBuildTestSuite);
+
+ #
+ # Instantiate the specified scheduler and let it do the rest.
+ #
+ oSchedGrpData = SchedGroupData().initFromDbWithId(oDb, oTestSet.idSchedGroup, oTestSet.tsCreated);
+ assert oSchedGrpData.fEnabled is True;
+ assert oSchedGrpData.idBuildSrc is not None;
+ oScheduler = SchedulerBase._instantiate(oDb, oSchedGrpData, iVerbosity);
+
+ return oScheduler.composeExecResponseWorker(idTestSet, oTestEx, oTestBox, oBuild, oValidationKitBuild, sBaseUrl);
+
+
+ def _updateTask(self, oTask, tsNow):
+ """
+ Updates a gang schedule task.
+ """
+ assert oTask.cMissingGangMembers >= 1;
+ assert oTask.idTestSetGangLeader is not None;
+ assert oTask.idTestSetGangLeader >= 1;
+ if tsNow is not None:
+ self._oDb.execute('UPDATE SchedQueues\n'
+ ' SET idTestSetGangLeader = %s,\n'
+ ' cMissingGangMembers = %s,\n'
+ ' tsLastScheduled = %s\n'
+ 'WHERE idItem = %s\n'
+ , (oTask.idTestSetGangLeader, oTask.cMissingGangMembers, tsNow, oTask.idItem,) );
+ else:
+ self._oDb.execute('UPDATE SchedQueues\n'
+ ' SET cMissingGangMembers = %s\n'
+ 'WHERE idItem = %s\n'
+ , (oTask.cMissingGangMembers, oTask.idItem,) );
+ return True;
+
+ def _moveTaskToEndOfQueue(self, oTask, cGangMembers, tsNow):
+ """
+ The task has been scheduled successfully, reset it's data move it to
+ the end of the queue.
+ """
+ if cGangMembers > 1:
+ self._oDb.execute('UPDATE SchedQueues\n'
+ ' SET idItem = NEXTVAL(\'SchedQueueItemIdSeq\'),\n'
+ ' idTestSetGangLeader = NULL,\n'
+ ' cMissingGangMembers = %s\n'
+ 'WHERE idItem = %s\n'
+ , (cGangMembers, oTask.idItem,) );
+ else:
+ self._oDb.execute('UPDATE SchedQueues\n'
+ ' SET idItem = NEXTVAL(\'SchedQueueItemIdSeq\'),\n'
+ ' idTestSetGangLeader = NULL,\n'
+ ' cMissingGangMembers = 1,\n'
+ ' tsLastScheduled = %s\n'
+ 'WHERE idItem = %s\n'
+ , (tsNow, oTask.idItem,) );
+ return True;
+
+
+
+
+ def _createTestSet(self, oTask, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, tsNow):
+ # type: (SchedQueueData, TestCaseArgsDataEx, TestBoxData, BuildDataEx, BuildDataEx, datetime.datetime) -> int
+ """
+ Creates a test set for using the given data.
+ Will not commit, someone up the callstack will that later on.
+
+ Returns the test set ID, may raise an exception on database error.
+ """
+ # Lazy bird doesn't want to write testset.py and does it all here.
+
+ #
+ # We're getting the TestSet ID first in order to include it in the base
+ # file name (that way we can directly relate files on the disk to the
+ # test set when doing batch work), and also for idTesetSetGangLeader.
+ #
+ self._oDb.execute('SELECT NEXTVAL(\'TestSetIdSeq\')');
+ idTestSet = self._oDb.fetchOne()[0];
+
+ sBaseFilename = '%04d/%02d/%02d/%02d/TestSet-%s' \
+ % (tsNow.year, tsNow.month, tsNow.day, (tsNow.hour // 6) * 6, idTestSet);
+
+ #
+ # Gang scheduling parameters. Changes the oTask data for updating by caller.
+ #
+ iGangMemberNo = 0;
+
+ if oTestEx.cGangMembers <= 1:
+ assert oTask.idTestSetGangLeader is None;
+ assert oTask.cMissingGangMembers <= 1;
+ elif oTask.idTestSetGangLeader is None:
+ assert oTask.cMissingGangMembers == oTestEx.cGangMembers;
+ oTask.cMissingGangMembers = oTestEx.cGangMembers - 1;
+ oTask.idTestSetGangLeader = idTestSet;
+ else:
+ assert oTask.cMissingGangMembers > 0 and oTask.cMissingGangMembers < oTestEx.cGangMembers;
+ oTask.cMissingGangMembers -= 1;
+
+ #
+ # Do the database stuff.
+ #
+ self._oDb.execute('INSERT INTO TestSets (\n'
+ ' idTestSet,\n'
+ ' tsConfig,\n'
+ ' tsCreated,\n'
+ ' idBuild,\n'
+ ' idBuildCategory,\n'
+ ' idBuildTestSuite,\n'
+ ' idGenTestBox,\n'
+ ' idTestBox,\n'
+ ' idSchedGroup,\n'
+ ' idTestGroup,\n'
+ ' idGenTestCase,\n'
+ ' idTestCase,\n'
+ ' idGenTestCaseArgs,\n'
+ ' idTestCaseArgs,\n'
+ ' sBaseFilename,\n'
+ ' iGangMemberNo,\n'
+ ' idTestSetGangLeader )\n'
+ 'VALUES ( %s,\n' # idTestSet
+ ' %s,\n' # tsConfig
+ ' %s,\n' # tsCreated
+ ' %s,\n' # idBuild
+ ' %s,\n' # idBuildCategory
+ ' %s,\n' # idBuildTestSuite
+ ' %s,\n' # idGenTestBox
+ ' %s,\n' # idTestBox
+ ' %s,\n' # idSchedGroup
+ ' %s,\n' # idTestGroup
+ ' %s,\n' # idGenTestCase
+ ' %s,\n' # idTestCase
+ ' %s,\n' # idGenTestCaseArgs
+ ' %s,\n' # idTestCaseArgs
+ ' %s,\n' # sBaseFilename
+ ' %s,\n' # iGangMemberNo
+ ' %s)\n' # idTestSetGangLeader
+ , ( idTestSet,
+ oTask.tsConfig,
+ tsNow,
+ oBuild.idBuild,
+ oBuild.idBuildCategory,
+ oValidationKitBuild.idBuild if oValidationKitBuild is not None else None,
+ oTestBoxData.idGenTestBox,
+ oTestBoxData.idTestBox,
+ oTask.idSchedGroup,
+ oTask.idTestGroup,
+ oTestEx.oTestCase.idGenTestCase,
+ oTestEx.oTestCase.idTestCase,
+ oTestEx.idGenTestCaseArgs,
+ oTestEx.idTestCaseArgs,
+ sBaseFilename,
+ iGangMemberNo,
+ oTask.idTestSetGangLeader,
+ ));
+
+ self._oDb.execute('INSERT INTO TestResults (\n'
+ ' idTestResultParent,\n'
+ ' idTestSet,\n'
+ ' tsCreated,\n'
+ ' idStrName,\n'
+ ' cErrors,\n'
+ ' enmStatus,\n'
+ ' iNestingDepth)\n'
+ 'VALUES ( NULL,\n' # idTestResultParent
+ ' %s,\n' # idTestSet
+ ' %s,\n' # tsCreated
+ ' 0,\n' # idStrName
+ ' 0,\n' # cErrors
+ ' \'running\'::TestStatus_T,\n'
+ ' 0)\n' # iNestingDepth
+ 'RETURNING idTestResult'
+ , ( idTestSet, tsNow, ));
+ idTestResult = self._oDb.fetchOne()[0];
+
+ self._oDb.execute('UPDATE TestSets\n'
+ ' SET idTestResult = %s\n'
+ 'WHERE idTestSet = %s\n'
+ , (idTestResult, idTestSet, ));
+
+ return idTestSet;
+
+ def _tryFindValidationKitBit(self, oTestBoxData, tsNow):
+ """
+ Tries to find the most recent validation kit build suitable for the given testbox.
+ Returns BuildDataEx or None. Raise exception on database error.
+
+ Can be overridden by child classes to change the default build requirements.
+ """
+ oBuildLogic = BuildLogic(self._oDb);
+ oBuildSource = BuildSourceData().initFromDbWithId(self._oDb, self._oSchedGrpData.idBuildSrcTestSuite, tsNow);
+ oCursor = BuildSourceLogic(self._oDb).openBuildCursor(oBuildSource, oTestBoxData.sOs, oTestBoxData.sCpuArch, tsNow);
+ for _ in range(oCursor.getRowCount()):
+ oBuild = BuildDataEx().initFromDbRow(oCursor.fetchOne());
+ if not oBuildLogic.isBuildBlacklisted(oBuild):
+ return oBuild;
+ return None;
+
+ def _tryFindBuild(self, oTask, oTestEx, oTestBoxData, tsNow):
+ """
+ Tries to find a fitting build.
+ Returns BuildDataEx or None. Raise exception on database error.
+
+ Can be overridden by child classes to change the default build requirements.
+ """
+
+ #
+ # Gather the set of prerequisites we have and turn them into a value
+ # set for use in the loop below.
+ #
+ # Note! We're scheduling on testcase level and ignoring argument variation
+ # selections in TestGroupMembers is intentional.
+ #
+ dPreReqs = {};
+
+ # Direct prerequisites. We assume they're all enabled as this can be
+ # checked at queue creation time.
+ for oPreReq in oTestEx.aoTestCasePreReqs:
+ dPreReqs[oPreReq.idTestCase] = 1;
+
+ # Testgroup dependencies from the scheduling group config.
+ if oTask.aidTestGroupPreReqs is not None:
+ for iTestGroup in oTask.aidTestGroupPreReqs:
+ # Make sure the _active_ test group members are in the cache.
+ if iTestGroup not in self.dTestGroupMembers:
+ self._oDb.execute('SELECT DISTINCT TestGroupMembers.idTestCase\n'
+ 'FROM TestGroupMembers, TestCases\n'
+ 'WHERE TestGroupMembers.idTestGroup = %s\n'
+ ' AND TestGroupMembers.tsExpire > %s\n'
+ ' AND TestGroupMembers.tsEffective <= %s\n'
+ ' AND TestCases.idTestCase = TestGroupMembers.idTestCase\n'
+ ' AND TestCases.tsExpire > %s\n'
+ ' AND TestCases.tsEffective <= %s\n'
+ ' AND TestCases.fEnabled is TRUE\n'
+ , (iTestGroup, oTask.tsConfig, oTask.tsConfig, oTask.tsConfig, oTask.tsConfig,));
+ aidTestCases = [];
+ for aoRow in self._oDb.fetchAll():
+ aidTestCases.append(aoRow[0]);
+ self.dTestGroupMembers[iTestGroup] = aidTestCases;
+
+ # Add the testgroup members to the prerequisites.
+ for idTestCase in self.dTestGroupMembers[iTestGroup]:
+ dPreReqs[idTestCase] = 1;
+
+ # Create a SQL values table out of them.
+ sPreReqSet = ''
+ if dPreReqs:
+ for idPreReq in sorted(dPreReqs):
+ sPreReqSet += ', (' + str(idPreReq) + ')';
+ sPreReqSet = sPreReqSet[2:]; # drop the leading ', '.
+
+ #
+ # Try the builds.
+ #
+ self.oBuildCache.setupSource(self._oDb, self._oSchedGrpData.idBuildSrc, oTestBoxData.sOs, oTestBoxData.sCpuArch, tsNow);
+ for oEntry in self.oBuildCache:
+ #
+ # Check build requirements set by the test.
+ #
+ if not oTestEx.matchesBuildProps(oEntry.oBuild):
+ continue;
+
+ if oEntry.isBlacklisted(self._oDb):
+ oEntry.remove();
+ continue;
+
+ #
+ # Check prerequisites. The default scheduler is satisfied if one
+ # argument variation has been executed successfully. It is not
+ # satisfied if there are any failure runs.
+ #
+ if sPreReqSet:
+ fDecision = oEntry.getPreReqDecision(sPreReqSet);
+ if fDecision is None:
+ # Check for missing prereqs.
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM (VALUES ' + sPreReqSet + ') AS PreReqs(idTestCase)\n'
+ 'LEFT OUTER JOIN (SELECT idTestSet\n'
+ ' FROM TestSets\n'
+ ' WHERE enmStatus IN (%s, %s)\n'
+ ' AND idBuild = %s\n'
+ ' ) AS TestSets\n'
+ ' ON (PreReqs.idTestCase = TestSets.idTestCase)\n'
+ 'WHERE TestSets.idTestSet is NULL\n'
+ , ( TestSetData.ksTestStatus_Success, TestSetData.ksTestStatus_Skipped,
+ oEntry.oBuild.idBuild, ));
+ cMissingPreReqs = self._oDb.fetchOne()[0];
+ if cMissingPreReqs > 0:
+ self.dprint('build %s is missing %u prerequisites (out of %s)'
+ % (oEntry.oBuild.idBuild, cMissingPreReqs, sPreReqSet,));
+ oEntry.setPreReqDecision(sPreReqSet, False);
+ continue;
+
+ # Check for failed prereq runs.
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM (VALUES ' + sPreReqSet + ') AS PreReqs(idTestCase),\n'
+ ' TestSets\n'
+ 'WHERE PreReqs.idTestCase = TestSets.idTestCase\n'
+ ' AND TestSets.idBuild = %s\n'
+ ' AND TestSets.enmStatus IN (%s, %s, %s)\n'
+ , ( oEntry.oBuild.idBuild,
+ TestSetData.ksTestStatus_Failure,
+ TestSetData.ksTestStatus_TimedOut,
+ TestSetData.ksTestStatus_Rebooted,
+ )
+ );
+ cFailedPreReqs = self._oDb.fetchOne()[0];
+ if cFailedPreReqs > 0:
+ self.dprint('build %s is has %u prerequisite failures (out of %s)'
+ % (oEntry.oBuild.idBuild, cFailedPreReqs, sPreReqSet,));
+ oEntry.setPreReqDecision(sPreReqSet, False);
+ continue;
+
+ oEntry.setPreReqDecision(sPreReqSet, True);
+ elif not fDecision:
+ continue;
+
+ #
+ # If we can, check if the build files still exist.
+ #
+ if oEntry.oBuild.areFilesStillThere() is False:
+ self.dprint('build %s no longer exists' % (oEntry.oBuild.idBuild,));
+ oEntry.remove();
+ continue;
+
+ self.dprint('found oBuild=%s' % (oEntry.oBuild,));
+ return oEntry.oBuild;
+ return None;
+
+ def _tryFindMatchingBuild(self, oLeaderBuild, oTestBoxData, idBuildSrc):
+ """
+ Tries to find a matching build for gang scheduling.
+ Returns BuildDataEx or None. Raise exception on database error.
+
+ Can be overridden by child classes to change the default build requirements.
+ """
+ #
+ # Note! Should probably check build prerequisites if we get a different
+ # build back, so that we don't use a build which hasn't passed
+ # the smoke test.
+ #
+ _ = idBuildSrc;
+ return BuildLogic(self._oDb).tryFindSameBuildForOsArch(oLeaderBuild, oTestBoxData.sOs, oTestBoxData.sCpuArch);
+
+
+ def _tryAsLeader(self, oTask, oTestEx, oTestBoxData, tsNow, sBaseUrl):
+ """
+ Try schedule the task as a gang leader (can be a gang of one).
+ Returns response or None. May raise exception on DB error.
+ """
+
+ # We don't wait for busy resources, we just try the next test.
+ oTestArgsLogic = TestCaseArgsLogic(self._oDb);
+ if not oTestArgsLogic.areResourcesFree(oTestEx):
+ self.dprint('Cannot get global test resources!');
+ return None;
+
+ #
+ # Find a matching build (this is the difficult bit).
+ #
+ oBuild = self._tryFindBuild(oTask, oTestEx, oTestBoxData, tsNow);
+ if oBuild is None:
+ self.dprint('No build!');
+ return None;
+ if oTestEx.oTestCase.needValidationKitBit():
+ oValidationKitBuild = self._tryFindValidationKitBit(oTestBoxData, tsNow);
+ if oValidationKitBuild is None:
+ self.dprint('No validation kit build!');
+ return None;
+ else:
+ oValidationKitBuild = None;
+
+ #
+ # Create a testset, allocate the resources and update the state.
+ # Note! Since resource allocation may still fail, we create a nested
+ # transaction so we can roll back. (Heed lock warning in docs!)
+ #
+ self._oDb.execute('SAVEPOINT tryAsLeader');
+ idTestSet = self._createTestSet(oTask, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, tsNow);
+
+ if GlobalResourceLogic(self._oDb).allocateResources(oTestBoxData.idTestBox, oTestEx.aoGlobalRsrc, fCommit = False) \
+ is not True:
+ self._oDb.execute('ROLLBACK TO SAVEPOINT tryAsLeader');
+ self.dprint('Failed to allocate global resources!');
+ return False;
+
+ if oTestEx.cGangMembers <= 1:
+ # We're alone, put the task back at the end of the queue and issue EXEC cmd.
+ self._moveTaskToEndOfQueue(oTask, oTestEx.cGangMembers, tsNow);
+ dResponse = self.composeExecResponseWorker(idTestSet, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, sBaseUrl);
+ sTBState = TestBoxStatusData.ksTestBoxState_Testing;
+ else:
+ # We're missing gang members, issue WAIT cmd.
+ self._updateTask(oTask, tsNow if idTestSet == oTask.idTestSetGangLeader else None);
+ dResponse = { constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_WAIT, };
+ sTBState = TestBoxStatusData.ksTestBoxState_GangGathering;
+
+ TestBoxStatusLogic(self._oDb).updateState(oTestBoxData.idTestBox, sTBState, idTestSet, fCommit = False);
+ self._oDb.execute('RELEASE SAVEPOINT tryAsLeader');
+ return dResponse;
+
+ def _tryAsGangMember(self, oTask, oTestEx, oTestBoxData, tsNow, sBaseUrl):
+ """
+ Try schedule the task as a gang member.
+ Returns response or None. May raise exception on DB error.
+ """
+
+ #
+ # The leader has choosen a build, we need to find a matching one for our platform.
+ # (It's up to the scheduler decide upon how strict dependencies are to be enforced
+ # upon subordinate group members.)
+ #
+ oLeaderTestSet = TestSetData().initFromDbWithId(self._oDb, oTestBoxData.idTestSetGangLeader);
+
+ oLeaderBuild = BuildDataEx().initFromDbWithId(self._oDb, oLeaderTestSet.idBuild);
+ oBuild = self._tryFindMatchingBuild(oLeaderBuild, oTestBoxData, self._oSchedGrpData.idBuildSrc);
+ if oBuild is None:
+ return None;
+
+ oValidationKitBuild = None;
+ if oLeaderTestSet.idBuildTestSuite is not None:
+ oLeaderValidationKitBit = BuildDataEx().initFromDbWithId(self._oDb, oLeaderTestSet.idBuildTestSuite);
+ oValidationKitBuild = self._tryFindMatchingBuild(oLeaderValidationKitBit, oTestBoxData,
+ self._oSchedGrpData.idBuildSrcTestSuite);
+
+ #
+ # Create a testset and update the state(s).
+ #
+ idTestSet = self._createTestSet(oTask, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, tsNow);
+
+ oTBStatusLogic = TestBoxStatusLogic(self._oDb);
+ if oTask.cMissingGangMembers < 1:
+ # The whole gang is there, move the task to the end of the queue
+ # and update the status on the other gang members.
+ self._moveTaskToEndOfQueue(oTask, oTestEx.cGangMembers, tsNow);
+ dResponse = self.composeExecResponseWorker(idTestSet, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, sBaseUrl);
+ sTBState = TestBoxStatusData.ksTestBoxState_GangTesting;
+ oTBStatusLogic.updateGangStatus(oTask.idTestSetGangLeader, sTBState, fCommit = False);
+ else:
+ # We're still missing some gang members, issue WAIT cmd.
+ self._updateTask(oTask, tsNow if idTestSet == oTask.idTestSetGangLeader else None);
+ dResponse = { constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_WAIT, };
+ sTBState = TestBoxStatusData.ksTestBoxState_GangGathering;
+
+ oTBStatusLogic.updateState(oTestBoxData.idTestBox, sTBState, idTestSet, fCommit = False);
+ return dResponse;
+
+
+ def scheduleNewTaskWorker(self, oTestBoxData, tsNow, sBaseUrl):
+ """
+ Worker for schduling a new task.
+ """
+
+ #
+ # Iterate the scheduler queue (fetch all to avoid having to concurrent
+ # queries), trying out each task to see if the testbox can execute it.
+ #
+ dRejected = {}; # variations we've already checked out and rejected.
+ self._oDb.execute('SELECT *\n'
+ 'FROM SchedQueues\n'
+ 'WHERE idSchedGroup = %s\n'
+ ' AND ( bmHourlySchedule IS NULL\n'
+ ' OR get_bit(bmHourlySchedule, %s) = 1 )\n'
+ 'ORDER BY idItem ASC\n'
+ , (self._oSchedGrpData.idSchedGroup, utils.getLocalHourOfWeek()) );
+ aaoRows = self._oDb.fetchAll();
+ for aoRow in aaoRows:
+ # Don't loop forever.
+ if self.getElapsedSecs() >= config.g_kcSecMaxNewTask:
+ break;
+
+ # Unpack the data and check if we've rejected the testcasevar/group variation already (they repeat).
+ oTask = SchedQueueData().initFromDbRow(aoRow);
+ if config.g_kfSrvGlueDebugScheduler:
+ self.dprint('** Considering: idItem=%s idGenTestCaseArgs=%s idTestGroup=%s Deps=%s last=%s cfg=%s\n'
+ % ( oTask.idItem, oTask.idGenTestCaseArgs, oTask.idTestGroup, oTask.aidTestGroupPreReqs,
+ oTask.tsLastScheduled, oTask.tsConfig,));
+
+ sRejectNm = '%s:%s' % (oTask.idGenTestCaseArgs, oTask.idTestGroup,);
+ if sRejectNm in dRejected:
+ self.dprint('Duplicate, already rejected! (%s)' % (sRejectNm,));
+ continue;
+ dRejected[sRejectNm] = 1;
+
+ # Fetch all the test case info (too much, but who cares right now).
+ oTestEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(self._oDb, oTask.idGenTestCaseArgs,
+ tsConfigEff = oTask.tsConfig,
+ tsRsrcEff = oTask.tsConfig);
+ if config.g_kfSrvGlueDebugScheduler:
+ self.dprint('TestCase "%s": %s %s' % (oTestEx.oTestCase.sName, oTestEx.oTestCase.sBaseCmd, oTestEx.sArgs,));
+
+ # This shouldn't happen, but just in case it does...
+ if oTestEx.oTestCase.fEnabled is not True:
+ self.dprint('Testcase is not enabled!!');
+ continue;
+
+ # Check if the testbox properties matches the test.
+ if not oTestEx.matchesTestBoxProps(oTestBoxData):
+ self.dprint('Testbox mismatch!');
+ continue;
+
+ # Try schedule it.
+ if oTask.idTestSetGangLeader is None or oTestEx.cGangMembers <= 1:
+ dResponse = self._tryAsLeader(oTask, oTestEx, oTestBoxData, tsNow, sBaseUrl);
+ elif oTask.cMissingGangMembers > 1:
+ dResponse = self._tryAsGangMember(oTask, oTestEx, oTestBoxData, tsNow, sBaseUrl);
+ else:
+ dResponse = None; # Shouldn't happen!
+ if dResponse is not None:
+ self.dprint('Found a task! dResponse=%s' % (dResponse,));
+ return dResponse;
+
+ # Found no suitable task.
+ return None;
+
+ @staticmethod
+ def _pickSchedGroup(oTestBoxDataEx, iWorkItem, dIgnoreSchedGroupIds):
+ """
+ Picks the next scheduling group for the given testbox.
+ """
+ if len(oTestBoxDataEx.aoInSchedGroups) == 1:
+ oSchedGroup = oTestBoxDataEx.aoInSchedGroups[0].oSchedGroup;
+ if oSchedGroup.fEnabled \
+ and oSchedGroup.idBuildSrc is not None \
+ and oSchedGroup.idSchedGroup not in dIgnoreSchedGroupIds:
+ return (oSchedGroup, 0);
+ iWorkItem = 0;
+
+ elif oTestBoxDataEx.aoInSchedGroups:
+ # Construct priority table of currently enabled scheduling groups.
+ aaoList1 = [];
+ for oInGroup in oTestBoxDataEx.aoInSchedGroups:
+ oSchedGroup = oInGroup.oSchedGroup;
+ if oSchedGroup.fEnabled and oSchedGroup.idBuildSrc is not None:
+ iSchedPriority = oInGroup.iSchedPriority;
+ if iSchedPriority > 31: # paranoia
+ iSchedPriority = 31;
+ elif iSchedPriority < 0: # paranoia
+ iSchedPriority = 0;
+
+ for iSchedPriority in xrange(min(iSchedPriority, len(aaoList1))):
+ aaoList1[iSchedPriority].append(oSchedGroup);
+ while len(aaoList1) <= iSchedPriority:
+ aaoList1.append([oSchedGroup,]);
+
+ # Flatten it into a single list, mixing the priorities a little so it doesn't
+ # take forever before low priority stuff is executed.
+ aoFlat = [];
+ iLo = 0;
+ iHi = len(aaoList1) - 1;
+ while iHi >= iLo:
+ aoFlat += aaoList1[iHi];
+ if iLo < iHi:
+ aoFlat += aaoList1[iLo];
+ iLo += 1;
+ iHi -= 1;
+
+ # Pick the next one.
+ cLeft = len(aoFlat);
+ while cLeft > 0:
+ cLeft -= 1;
+ iWorkItem += 1;
+ if iWorkItem >= len(aoFlat) or iWorkItem < 0:
+ iWorkItem = 0;
+ if aoFlat[iWorkItem].idSchedGroup not in dIgnoreSchedGroupIds:
+ return (aoFlat[iWorkItem], iWorkItem);
+ else:
+ iWorkItem = 0;
+
+ # No active group.
+ return (None, iWorkItem);
+
+ @staticmethod
+ def scheduleNewTask(oDb, oTestBoxData, iWorkItem, sBaseUrl, iVerbosity = 0):
+ # type: (TMDatabaseConnection, TestBoxData, int, str, int) -> None
+ """
+ Schedules a new task for a testbox.
+ """
+ oTBStatusLogic = TestBoxStatusLogic(oDb);
+
+ try:
+ #
+ # To avoid concurrency issues in SchedQueues we lock all the rows
+ # related to our scheduling queue. Also, since this is a very
+ # expensive operation we lock the testbox status row to fend of
+ # repeated retires by faulty testbox scripts.
+ #
+ tsSecStart = utils.timestampSecond();
+ oDb.rollback();
+ oDb.begin();
+ oDb.execute('SELECT idTestBox FROM TestBoxStatuses WHERE idTestBox = %s FOR UPDATE NOWAIT'
+ % (oTestBoxData.idTestBox,));
+ oDb.execute('SELECT SchedQueues.idSchedGroup\n'
+ ' FROM SchedQueues, TestBoxesInSchedGroups\n'
+ 'WHERE TestBoxesInSchedGroups.idTestBox = %s\n'
+ ' AND TestBoxesInSchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestBoxesInSchedGroups.idSchedGroup = SchedQueues.idSchedGroup\n'
+ ' FOR UPDATE'
+ % (oTestBoxData.idTestBox,));
+
+ # We need the current timestamp.
+ tsNow = oDb.getCurrentTimestamp();
+
+ # Re-read the testbox data with scheduling group relations.
+ oTestBoxDataEx = TestBoxDataEx().initFromDbWithId(oDb, oTestBoxData.idTestBox, tsNow);
+ if oTestBoxDataEx.fEnabled \
+ and oTestBoxDataEx.idGenTestBox == oTestBoxData.idGenTestBox:
+
+ # We may have to skip scheduling groups that are out of work (e.g. 'No build').
+ iInitialWorkItem = iWorkItem;
+ dIgnoreSchedGroupIds = {};
+ while True:
+ # Now, pick the scheduling group.
+ (oSchedGroup, iWorkItem) = SchedulerBase._pickSchedGroup(oTestBoxDataEx, iWorkItem, dIgnoreSchedGroupIds);
+ if oSchedGroup is None:
+ break;
+ assert oSchedGroup.fEnabled and oSchedGroup.idBuildSrc is not None;
+
+ # Instantiate the specified scheduler and let it do the rest.
+ oScheduler = SchedulerBase._instantiate(oDb, oSchedGroup, iVerbosity, tsSecStart);
+ dResponse = oScheduler.scheduleNewTaskWorker(oTestBoxDataEx, tsNow, sBaseUrl);
+ if dResponse is not None:
+ oTBStatusLogic.updateWorkItem(oTestBoxDataEx.idTestBox, iWorkItem);
+ oDb.commit();
+ return dResponse;
+
+ # Check out the next work item?
+ if oScheduler.getElapsedSecs() > config.g_kcSecMaxNewTask:
+ break;
+ dIgnoreSchedGroupIds[oSchedGroup.idSchedGroup] = oSchedGroup;
+
+ # No luck, but best if we update the work item if we've made progress.
+ # Note! In case of a config.g_kcSecMaxNewTask timeout, this may accidentally skip
+ # a work item with actually work to do. But that's a small price to pay.
+ if iWorkItem != iInitialWorkItem:
+ oTBStatusLogic.updateWorkItem(oTestBoxDataEx.idTestBox, iWorkItem);
+ oDb.commit();
+ return None;
+ except:
+ oDb.rollback();
+ raise;
+
+ # Not enabled, rollback and return no task.
+ oDb.rollback();
+ return None;
+
+ @staticmethod
+ def tryCancelGangGathering(oDb, oStatusData):
+ """
+ Try canceling a gang gathering.
+
+ Returns True if successfully cancelled.
+ Returns False if not (someone raced us to the SchedQueue table).
+
+ Note! oStatusData is re-initialized.
+ """
+ assert oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGathering;
+ try:
+ #
+ # Lock the tables we're updating so we don't run into concurrency
+ # issues (we're racing both scheduleNewTask and other callers of
+ # this method).
+ #
+ oDb.rollback();
+ oDb.begin();
+ oDb.execute('LOCK TABLE TestBoxStatuses, SchedQueues IN EXCLUSIVE MODE');
+
+ #
+ # Re-read the testbox data and check that we're still in the same state.
+ #
+ oStatusData.initFromDbWithId(oDb, oStatusData.idTestBox);
+ if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGathering:
+ #
+ # Get the leader thru the test set and change the state of the whole gang.
+ #
+ oTestSetData = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
+
+ oTBStatusLogic = TestBoxStatusLogic(oDb);
+ oTBStatusLogic.updateGangStatus(oTestSetData.idTestSetGangLeader,
+ TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut,
+ fCommit = False);
+
+ #
+ # Move the scheduling queue item to the end.
+ #
+ oDb.execute('SELECT *\n'
+ 'FROM SchedQueues\n'
+ 'WHERE idTestSetGangLeader = %s\n'
+ , (oTestSetData.idTestSetGangLeader,) );
+ oTask = SchedQueueData().initFromDbRow(oDb.fetchOne());
+ oTestEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(oDb, oTask.idGenTestCaseArgs,
+ tsConfigEff = oTask.tsConfig,
+ tsRsrcEff = oTask.tsConfig);
+ oDb.execute('UPDATE SchedQueues\n'
+ ' SET idItem = NEXTVAL(\'SchedQueueItemIdSeq\'),\n'
+ ' idTestSetGangLeader = NULL,\n'
+ ' cMissingGangMembers = %s\n'
+ 'WHERE idItem = %s\n'
+ , (oTestEx.cGangMembers, oTask.idItem,) );
+
+ oDb.commit();
+ return True;
+
+ if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut:
+ oDb.rollback();
+ return True;
+ except:
+ oDb.rollback();
+ raise;
+
+ # Not enabled, rollback and return no task.
+ oDb.rollback();
+ return False;
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class SchedQueueDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [SchedQueueData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/schedulerbeci.py b/src/VBox/ValidationKit/testmanager/core/schedulerbeci.py
new file mode 100755
index 00000000..5cd800d5
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/schedulerbeci.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+# $Id: schedulerbeci.py $
+
+"""
+Test Manager - Best-Effort-Continuous-Integration (BECI) scheduler.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.core.schedulerbase import SchedulerBase, SchedQueueData;
+
+
+class SchdulerBeci(SchedulerBase): # pylint: disable=too-few-public-methods
+ """
+ The best-effort-continuous-integration scheduler, BECI for short.
+ """
+
+ def __init__(self, oDb, oSchedGrpData, iVerbosity, tsSecStart):
+ SchedulerBase.__init__(self, oDb, oSchedGrpData, iVerbosity, tsSecStart);
+
+ def _recreateQueueItems(self, oData):
+ #
+ # Prepare the input data for the loop below. We compress the priority
+ # to reduce the number of loops we need to executes below.
+ #
+ # Note! For BECI test group priority only applies to the ordering of
+ # test groups, which has been resolved by the done sorting in the
+ # base class.
+ #
+ iMinPriority = 0x7fff;
+ iMaxPriority = 0;
+ for oTestGroup in oData.aoTestGroups:
+ for oTestCase in oTestGroup.aoTestCases:
+ iPrio = oTestCase.iSchedPriority;
+ assert iPrio in range(32);
+ iPrio = iPrio // 4;
+ assert iPrio in range(8);
+ if iPrio > iMaxPriority:
+ iMaxPriority = iPrio;
+ if iPrio < iMinPriority:
+ iMinPriority = iPrio;
+
+ oTestCase.iBeciPrio = iPrio;
+ oTestCase.iNextVariation = -1;
+
+ assert iMinPriority in range(8);
+ assert iMaxPriority in range(8);
+ assert iMinPriority <= iMaxPriority;
+
+ #
+ # Generate the
+ #
+ cMaxItems = len(oData.aoArgsVariations) * 64;
+ cMaxItems = min(cMaxItems, 1048576);
+
+ aoItems = [];
+ cNotAtEnd = len(oData.aoTestCases);
+ while len(aoItems) < cMaxItems:
+ self.msgDebug('outer loop: %s items' % (len(aoItems),));
+ for iPrio in range(iMaxPriority, iMinPriority - 1, -1):
+ #self.msgDebug('prio loop: %s' % (iPrio,));
+ for oTestGroup in oData.aoTestGroups:
+ #self.msgDebug('testgroup loop: %s' % (oTestGroup,));
+ for oTestCase in oTestGroup.aoTestCases:
+ #self.msgDebug('testcase loop: idTestCase=%s' % (oTestCase.idTestCase,));
+ if iPrio <= oTestCase.iBeciPrio and oTestCase.aoArgsVariations:
+ # Get variation.
+ iNext = oTestCase.iNextVariation;
+ if iNext != 0:
+ if iNext == -1: iNext = 0;
+ cNotAtEnd -= 1;
+ oArgsVariation = oTestCase.aoArgsVariations[iNext];
+
+ # Update next variation.
+ iNext = (iNext + 1) % len(oTestCase.aoArgsVariations);
+ cNotAtEnd += iNext != 0;
+ oTestCase.iNextVariation = iNext;
+
+ # Create queue item and append it.
+ oItem = SchedQueueData();
+ oItem.initFromValues(idSchedGroup = self._oSchedGrpData.idSchedGroup,
+ idGenTestCaseArgs = oArgsVariation.idGenTestCaseArgs,
+ idTestGroup = oTestGroup.idTestGroup,
+ aidTestGroupPreReqs = oTestGroup.aidTestGroupPreReqs,
+ bmHourlySchedule = oTestGroup.bmHourlySchedule,
+ cMissingGangMembers = oArgsVariation.cGangMembers,
+ offQueue = len(aoItems));
+ aoItems.append(oItem);
+
+ # Done?
+ if cNotAtEnd == 0:
+ self.msgDebug('returns %s items' % (len(aoItems),));
+ return aoItems;
+ return aoItems;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/systemchangelog.py b/src/VBox/ValidationKit/testmanager/core/systemchangelog.py
new file mode 100755
index 00000000..7c85c76a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/systemchangelog.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+# $Id: systemchangelog.py $
+
+"""
+Test Manager - System changelog compilation.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.core.base import ModelLogicBase;
+from testmanager.core.useraccount import UserAccountLogic;
+from testmanager.core.systemlog import SystemLogData;
+
+
+class SystemChangelogEntry(object): # pylint: disable=too-many-instance-attributes
+ """
+ System changelog entry.
+ """
+
+ def __init__(self, tsEffective, oAuthor, sEvent, idWhat, sDesc):
+ self.tsEffective = tsEffective;
+ self.oAuthor = oAuthor;
+ self.sEvent = sEvent;
+ self.idWhat = idWhat;
+ self.sDesc = sDesc;
+
+
+class SystemChangelogLogic(ModelLogicBase):
+ """
+ System changelog compilation logic.
+ """
+
+ ## @name What kind of change.
+ ## @{
+ ksWhat_TestBox = 'chlog::TestBox';
+ ksWhat_TestCase = 'chlog::TestCase';
+ ksWhat_Blacklisting = 'chlog::Blacklisting';
+ ksWhat_Build = 'chlog::Build';
+ ksWhat_BuildSource = 'chlog::BuildSource';
+ ksWhat_FailureCategory = 'chlog::FailureCategory';
+ ksWhat_FailureReason = 'chlog::FailureReason';
+ ksWhat_GlobalRsrc = 'chlog::GlobalRsrc';
+ ksWhat_SchedGroup = 'chlog::SchedGroup';
+ ksWhat_TestGroup = 'chlog::TestGroup';
+ ksWhat_User = 'chlog::User';
+ ksWhat_TestResult = 'chlog::TestResult';
+ ## @}
+
+ ## Mapping a changelog entry kind to a table, key and clue.
+ kdWhatToTable = dict({ # pylint: disable=star-args
+ ksWhat_TestBox: ( 'TestBoxes', 'idTestBox', None, ),
+ ksWhat_TestCase: ( 'TestCasees', 'idTestCase', None, ),
+ ksWhat_Blacklisting: ( 'Blacklist', 'idBlacklisting', None, ),
+ ksWhat_Build: ( 'Builds', 'idBuild', None, ),
+ ksWhat_BuildSource: ( 'BuildSources', 'idBuildSrc', None, ),
+ ksWhat_FailureCategory: ( 'FailureCategories', 'idFailureCategory', None, ),
+ ksWhat_FailureReason: ( 'FailureReasons', 'idFailureReason', None, ),
+ ksWhat_GlobalRsrc: ( 'GlobalResources', 'idGlobalRsrc', None, ),
+ ksWhat_SchedGroup: ( 'SchedGroups', 'idSchedGroup', None, ),
+ ksWhat_TestGroup: ( 'TestGroups', 'idTestGroup', None, ),
+ ksWhat_User: ( 'Users', 'idUser', None, ),
+ ksWhat_TestResult: ( 'TestResults', 'idTestResult', None, ),
+ }, **{sEvent: ( 'SystemLog', 'tsCreated', 'TimestampId', ) for sEvent in SystemLogData.kasEvents});
+
+ ## The table key is the effective timestamp. (Can't be used above for some weird scoping reason.)
+ ksClue_TimestampId = 'TimestampId';
+
+ ## @todo move to config.py?
+ ksVSheriffLoginName = 'vsheriff';
+
+
+ ## @name for kaasChangelogTables
+ ## @internal
+ ## @{
+ ksTweak_None = '';
+ ksTweak_NotNullAuthor = 'uidAuthorNotNull';
+ ksTweak_NotNullAuthorOrVSheriff = 'uidAuthorNotNullOrVSheriff';
+ ## @}
+
+ ## @internal
+ kaasChangelogTables = (
+ # [0]: change name, [1]: Table name, [2]: key column, [3]:later, [4]: tweak
+ ( ksWhat_TestBox, 'TestBoxes', 'idTestBox', None, ksTweak_NotNullAuthor, ),
+ ( ksWhat_TestBox, 'TestBoxesInSchedGroups', 'idTestBox', None, ksTweak_None, ),
+ ( ksWhat_TestCase, 'TestCases', 'idTestCase', None, ksTweak_None, ),
+ ( ksWhat_TestCase, 'TestCaseArgs', 'idTestCase', None, ksTweak_None, ),
+ ( ksWhat_TestCase, 'TestCaseDeps', 'idTestCase', None, ksTweak_None, ),
+ ( ksWhat_TestCase, 'TestCaseGlobalRsrcDeps', 'idTestCase', None, ksTweak_None, ),
+ ( ksWhat_Blacklisting, 'BuildBlacklist', 'idBlacklisting', None, ksTweak_None, ),
+ ( ksWhat_Build, 'Builds', 'idBuild', None, ksTweak_NotNullAuthor, ),
+ ( ksWhat_BuildSource, 'BuildSources', 'idBuildSrc', None, ksTweak_None, ),
+ ( ksWhat_FailureCategory, 'FailureCategories', 'idFailureCategory', None, ksTweak_None, ),
+ ( ksWhat_FailureReason, 'FailureReasons', 'idFailureReason', None, ksTweak_None, ),
+ ( ksWhat_GlobalRsrc, 'GlobalResources', 'idGlobalRsrc', None, ksTweak_None, ),
+ ( ksWhat_SchedGroup, 'SchedGroups', 'idSchedGroup', None, ksTweak_None, ),
+ ( ksWhat_SchedGroup, 'SchedGroupMembers', 'idSchedGroup', None, ksTweak_None, ),
+ ( ksWhat_TestGroup, 'TestGroups', 'idTestGroup', None, ksTweak_None, ),
+ ( ksWhat_TestGroup, 'TestGroupMembers', 'idTestGroup', None, ksTweak_None, ),
+ ( ksWhat_User, 'Users', 'uid', None, ksTweak_None, ),
+ ( ksWhat_TestResult, 'TestResultFailures', 'idTestResult', None, ksTweak_NotNullAuthorOrVSheriff, ),
+ );
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb);
+
+
+ def fetchForListingEx(self, iStart, cMaxRows, tsNow, cDaysBack, aiSortColumns = None):
+ """
+ Fetches SystemLog entries.
+
+ Returns an array (list) of SystemLogData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+
+ #
+ # Construct the query.
+ #
+ oUserAccountLogic = UserAccountLogic(self._oDb);
+ oVSheriff = oUserAccountLogic.tryFetchAccountByLoginName(self.ksVSheriffLoginName);
+ uidVSheriff = oVSheriff.uid if oVSheriff is not None else -1;
+
+ if tsNow is None:
+ sWhereTime = self._oDb.formatBindArgs(' WHERE tsEffective >= CURRENT_TIMESTAMP - \'%s days\'::interval\n',
+ (cDaysBack,));
+ else:
+ sWhereTime = self._oDb.formatBindArgs(' WHERE tsEffective >= (%s::timestamptz - \'%s days\'::interval)\n'
+ ' AND tsEffective <= %s\n',
+ (tsNow, cDaysBack, tsNow));
+
+ # Special entry for the system log.
+ sQuery = '(\n'
+ sQuery += ' SELECT NULL AS uidAuthor,\n';
+ sQuery += ' tsCreated AS tsEffective,\n';
+ sQuery += ' sEvent AS sEvent,\n';
+ sQuery += ' NULL AS idWhat,\n';
+ sQuery += ' sLogText AS sDesc\n';
+ sQuery += ' FROM SystemLog\n';
+ sQuery += sWhereTime.replace('tsEffective', 'tsCreated');
+ sQuery += ' ORDER BY tsCreated DESC\n'
+ sQuery += ')'
+
+ for asEntry in self.kaasChangelogTables:
+ sQuery += ' UNION (\n'
+ sQuery += ' SELECT uidAuthor, tsEffective, \'' + asEntry[0] + '\', ' + asEntry[2] + ', \'\'\n';
+ sQuery += ' FROM ' + asEntry[1] + '\n'
+ sQuery += sWhereTime;
+ if asEntry[4] == self.ksTweak_NotNullAuthor or asEntry[4] == self.ksTweak_NotNullAuthorOrVSheriff:
+ sQuery += ' AND uidAuthor IS NOT NULL\n';
+ if asEntry[4] == self.ksTweak_NotNullAuthorOrVSheriff:
+ sQuery += ' AND uidAuthor <> %u\n' % (uidVSheriff,);
+ sQuery += ' ORDER BY tsEffective DESC\n'
+ sQuery += ')';
+ sQuery += ' ORDER BY 2 DESC\n';
+ sQuery += ' LIMIT %u OFFSET %u\n' % (cMaxRows, iStart, );
+
+
+ #
+ # Execute the query and construct the return data.
+ #
+ self._oDb.execute(sQuery);
+ aoRows = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(SystemChangelogEntry(aoRow[1], oUserAccountLogic.cachedLookup(aoRow[0]),
+ aoRow[2], aoRow[3], aoRow[4]));
+
+
+ return aoRows;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/systemlog.py b/src/VBox/ValidationKit/testmanager/core/systemlog.py
new file mode 100755
index 00000000..85929c51
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/systemlog.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+# $Id: systemlog.py $
+
+"""
+Test Manager - SystemLog.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase;
+
+
+class SystemLogData(ModelDataBase): # pylint: disable=too-many-instance-attributes
+ """
+ SystemLog Data.
+ """
+
+ ## @name Event Constants
+ # @{
+ ksEvent_CmdNacked = 'CmdNack ';
+ ksEvent_TestBoxUnknown = 'TBoxUnkn';
+ ksEvent_TestSetAbandoned = 'TSetAbdd';
+ ksEvent_UserAccountUnknown = 'TAccUnkn';
+ ksEvent_XmlResultMalformed = 'XmlRMalf';
+ ksEvent_SchedQueueRecreate = 'SchQRecr';
+ ## @}
+
+ ## Valid event types.
+ kasEvents = \
+ [ \
+ ksEvent_CmdNacked,
+ ksEvent_TestBoxUnknown,
+ ksEvent_TestSetAbandoned,
+ ksEvent_UserAccountUnknown,
+ ksEvent_XmlResultMalformed,
+ ksEvent_SchedQueueRecreate,
+ ];
+
+ ksParam_tsCreated = 'tsCreated';
+ ksParam_sEvent = 'sEvent';
+ ksParam_sLogText = 'sLogText';
+
+ kasValidValues_sEvent = kasEvents;
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.tsCreated = None;
+ self.sEvent = None;
+ self.sLogText = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Internal worker for initFromDbWithId and initFromDbWithGenId as well as
+ SystemLogLogic.
+ """
+
+ if aoRow is None:
+ raise TMExceptionBase('SystemLog row not found.');
+
+ self.tsCreated = aoRow[0];
+ self.sEvent = aoRow[1];
+ self.sLogText = aoRow[2];
+ return self;
+
+
+class SystemLogLogic(ModelLogicBase):
+ """
+ SystemLog logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb);
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches SystemLog entries.
+
+ Returns an array (list) of SystemLogData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SystemLog\n'
+ 'ORDER BY tsCreated DESC\n'
+ 'LIMIT %s OFFSET %s\n',
+ (cMaxRows, iStart));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM SystemLog\n'
+ 'WHERE tsCreated <= %s\n'
+ 'ORDER BY tsCreated DESC\n'
+ 'LIMIT %s OFFSET %s\n',
+ (tsNow, cMaxRows, iStart));
+ aoRows = [];
+ for _ in range(self._oDb.getRowCount()):
+ oData = SystemLogData();
+ oData.initFromDbRow(self._oDb.fetchOne());
+ aoRows.append(oData);
+ return aoRows;
+
+ def addEntry(self, sEvent, sLogText, cHoursRepeat = 0, fCommit = False):
+ """
+ Adds an entry to the SystemLog table.
+ Raises exception on problem.
+ """
+ if sEvent not in SystemLogData.kasEvents:
+ raise TMExceptionBase('Unknown event type "%s"' % (sEvent,));
+
+ # Check the repeat restriction first.
+ if cHoursRepeat > 0:
+ self._oDb.execute('SELECT COUNT(*) as Stuff\n'
+ 'FROM SystemLog\n'
+ 'WHERE tsCreated >= (current_timestamp - interval \'%s hours\')\n'
+ ' AND sEvent = %s\n'
+ ' AND sLogText = %s\n',
+ (cHoursRepeat,
+ sEvent,
+ sLogText));
+ aRow = self._oDb.fetchOne();
+ if aRow[0] > 0:
+ return None;
+
+ # Insert it.
+ self._oDb.execute('INSERT INTO SystemLog (sEvent, sLogText)\n'
+ 'VALUES (%s, %s)\n',
+ (sEvent, sLogText));
+
+ if fCommit:
+ self._oDb.commit();
+ return True;
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class SystemLogDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [SystemLogData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testbox.pgsql b/src/VBox/ValidationKit/testmanager/core/testbox.pgsql
new file mode 100644
index 00000000..a6a085b8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testbox.pgsql
@@ -0,0 +1,635 @@
+-- $Id: testbox.pgsql $
+--- @file
+-- VBox Test Manager Database Stored Procedures - TestBoxes.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+--
+-- Old type signatures.
+--
+DROP FUNCTION IF EXISTS TestBoxLogic_addEntry(a_uidAuthor INTEGER,
+ a_ip inet,
+ a_uuidSystem uuid,
+ a_sName TEXT,
+ a_sDescription TEXT,
+ a_idSchedGroup INTEGER,
+ a_fEnabled BOOLEAN,
+ a_enmLomKind LomKind_T,
+ a_ipLom inet,
+ a_pctScaleTimeout INTEGER, -- Actually smallint, but default typing fun.
+ a_sComment TEXT,
+ a_enmPendingCmd TestBoxCmd_T,
+ OUT r_idTestBox INTEGER,
+ OUT r_idGenTestBox INTEGER,
+ OUT r_tsEffective TIMESTAMP WITH TIME ZONE);
+DROP FUNCTION IF EXISTS TestBoxLogic_editEntry(a_uidAuthor INTEGER,
+ a_idTestBox INTEGER,
+ a_ip inet,
+ a_uuidSystem uuid,
+ a_sName TEXT,
+ a_sDescription TEXT,
+ a_idSchedGroup INTEGER,
+ a_fEnabled BOOLEAN,
+ a_enmLomKind LomKind_T,
+ a_ipLom inet,
+ a_pctScaleTimeout INTEGER, -- Actually smallint, but default typing fun.
+ a_sComment TEXT,
+ a_enmPendingCmd TestBoxCmd_T,
+ OUT r_idGenTestBox INTEGER,
+ OUT r_tsEffective TIMESTAMP WITH TIME ZONE);
+DROP FUNCTION IF EXISTS TestBoxLogic_removeEntry(INTEGER, INTEGER, BOOLEAN);
+DROP FUNCTION IF EXISTS TestBoxLogic_addGroupEntry(a_uidAuthor INTEGER,
+ a_idTestBox INTEGER,
+ a_idSchedGroup INTEGER,
+ a_iSchedPriority INTEGER,
+ OUT r_tsEffective TIMESTAMP WITH TIME ZONE);
+DROP FUNCTION IF EXISTS TestBoxLogic_editGroupEntry(a_uidAuthor INTEGER,
+ a_idTestBox INTEGER,
+ a_idSchedGroup INTEGER,
+ a_iSchedPriority INTEGER,
+ OUT r_tsEffective INTEGER);
+
+
+---
+-- Checks if the test box name is unique, ignoring a_idTestCaseIgnore.
+-- Raises exception if duplicates are found.
+--
+-- @internal
+--
+CREATE OR REPLACE FUNCTION TestBoxLogic_checkUniqueName(a_sName TEXT, a_idTestBoxIgnore INTEGER)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cRows INTEGER;
+ BEGIN
+ SELECT COUNT(*) INTO v_cRows
+ FROM TestBoxes
+ WHERE sName = a_sName
+ AND tsExpire = 'infinity'::TIMESTAMP
+ AND idTestBox <> a_idTestBoxIgnore;
+ IF v_cRows <> 0 THEN
+ RAISE EXCEPTION 'Duplicate test box name "%" (% times)', a_sName, v_cRows;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Checks that the given scheduling group exists.
+-- Raises exception if it doesn't.
+--
+-- @internal
+--
+CREATE OR REPLACE FUNCTION TestBoxLogic_checkSchedGroupExists(a_idSchedGroup INTEGER)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cRows INTEGER;
+ BEGIN
+ SELECT COUNT(*) INTO v_cRows
+ FROM SchedGroups
+ WHERE idSchedGroup = a_idSchedGroup
+ AND tsExpire = 'infinity'::TIMESTAMP;
+ IF v_cRows <> 1 THEN
+ IF v_cRows = 0 THEN
+ RAISE EXCEPTION 'Scheduling group with ID % was not found', a_idSchedGroup;
+ END IF;
+ RAISE EXCEPTION 'Integrity error in SchedGroups: % current rows with idSchedGroup=%', v_cRows, a_idSchedGroup;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Checks that the given testbxo + scheduling group pair does not currently exists.
+-- Raises exception if it does.
+--
+-- @internal
+--
+CREATE OR REPLACE FUNCTION TestBoxLogic_checkTestBoxNotInSchedGroup(a_idTestBox INTEGER, a_idSchedGroup INTEGER)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cRows INTEGER;
+ BEGIN
+ SELECT COUNT(*) INTO v_cRows
+ FROM TestBoxesInSchedGroups
+ WHERE idTestBox = a_idTestBox
+ AND idSchedGroup = a_idSchedGroup
+ AND tsExpire = 'infinity'::TIMESTAMP;
+ IF v_cRows <> 0 THEN
+ RAISE EXCEPTION 'TestBox % is already a member of scheduling group %', a_idTestBox, a_idSchedGroup;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Historize a row.
+-- @internal
+--
+CREATE OR REPLACE FUNCTION TestBoxLogic_historizeEntry(a_idGenTestBox INTEGER, a_tsExpire TIMESTAMP WITH TIME ZONE)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cUpdatedRows INTEGER;
+ BEGIN
+ UPDATE TestBoxes
+ SET tsExpire = a_tsExpire
+ WHERE idGenTestBox = a_idGenTestBox
+ AND tsExpire = 'infinity'::TIMESTAMP;
+ GET DIAGNOSTICS v_cUpdatedRows = ROW_COUNT;
+ IF v_cUpdatedRows <> 1 THEN
+ IF v_cUpdatedRows = 0 THEN
+ RAISE EXCEPTION 'Test box generation ID % is no longer valid', a_idGenTestBox;
+ END IF;
+ RAISE EXCEPTION 'Integrity error in TestBoxes: % current rows with idGenTestBox=%', v_cUpdatedRows, a_idGenTestBox;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Historize a in-scheduling-group row.
+-- @internal
+--
+CREATE OR REPLACE FUNCTION TestBoxLogic_historizeGroupEntry(a_idTestBox INTEGER,
+ a_idSchedGroup INTEGER,
+ a_tsExpire TIMESTAMP WITH TIME ZONE)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cUpdatedRows INTEGER;
+ BEGIN
+ UPDATE TestBoxesInSchedGroups
+ SET tsExpire = a_tsExpire
+ WHERE idTestBox = a_idTestBox
+ AND idSchedGroup = a_idSchedGroup
+ AND tsExpire = 'infinity'::TIMESTAMP;
+ GET DIAGNOSTICS v_cUpdatedRows = ROW_COUNT;
+ IF v_cUpdatedRows <> 1 THEN
+ IF v_cUpdatedRows = 0 THEN
+ RAISE EXCEPTION 'TestBox ID % / SchedGroup ID % is no longer a valid combination', a_idTestBox, a_idSchedGroup;
+ END IF;
+ RAISE EXCEPTION 'Integrity error in TestBoxesInSchedGroups: % current rows for % / %',
+ v_cUpdatedRows, a_idTestBox, a_idSchedGroup;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Translate string via the string table.
+--
+-- @returns NULL if a_sValue is NULL, otherwise a string ID.
+--
+CREATE OR REPLACE FUNCTION TestBoxLogic_lookupOrFindString(a_sValue TEXT)
+ RETURNS INTEGER AS $$
+ DECLARE
+ v_idStr INTEGER;
+ v_cRows INTEGER;
+ BEGIN
+ IF a_sValue IS NULL THEN
+ RETURN NULL;
+ END IF;
+
+ SELECT idStr
+ INTO v_idStr
+ FROM TestBoxStrTab
+ WHERE sValue = a_sValue;
+ GET DIAGNOSTICS v_cRows = ROW_COUNT;
+ IF v_cRows = 0 THEN
+ INSERT INTO TestBoxStrTab (sValue)
+ VALUES (a_sValue)
+ RETURNING idStr INTO v_idStr;
+ END IF;
+ RETURN v_idStr;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Only adds the user settable parts of the row, i.e. not what TestBoxLogic_updateOnSignOn touches.
+--
+CREATE OR REPLACE function TestBoxLogic_addEntry(a_uidAuthor INTEGER,
+ a_ip inet,
+ a_uuidSystem uuid,
+ a_sName TEXT,
+ a_sDescription TEXT,
+ a_fEnabled BOOLEAN,
+ a_enmLomKind LomKind_T,
+ a_ipLom inet,
+ a_pctScaleTimeout INTEGER, -- Actually smallint, but default typing fun.
+ a_sComment TEXT,
+ a_enmPendingCmd TestBoxCmd_T,
+ OUT r_idTestBox INTEGER,
+ OUT r_idGenTestBox INTEGER,
+ OUT r_tsEffective TIMESTAMP WITH TIME ZONE
+ ) AS $$
+ DECLARE
+ v_idStrDescription INTEGER;
+ v_idStrComment INTEGER;
+ BEGIN
+ PERFORM TestBoxLogic_checkUniqueName(a_sName, -1);
+
+ SELECT TestBoxLogic_lookupOrFindString(a_sDescription) INTO v_idStrDescription;
+ SELECT TestBoxLogic_lookupOrFindString(a_sComment) INTO v_idStrComment;
+
+ INSERT INTO TestBoxes (
+ tsEffective, -- 1
+ uidAuthor, -- 2
+ ip, -- 3
+ uuidSystem, -- 4
+ sName, -- 5
+ idStrDescription, -- 6
+ fEnabled, -- 7
+ enmLomKind, -- 8
+ ipLom, -- 9
+ pctScaleTimeout, -- 10
+ idStrComment, -- 11
+ enmPendingCmd ) -- 12
+ VALUES (CURRENT_TIMESTAMP, -- 1
+ a_uidAuthor, -- 2
+ a_ip, -- 3
+ a_uuidSystem, -- 4
+ a_sName, -- 5
+ v_idStrDescription, -- 6
+ a_fEnabled, -- 7
+ a_enmLomKind, -- 8
+ a_ipLom, -- 9
+ a_pctScaleTimeout, -- 10
+ v_idStrComment, -- 11
+ a_enmPendingCmd ) -- 12
+ RETURNING idTestBox, idGenTestBox, tsEffective INTO r_idTestBox, r_idGenTestBox, r_tsEffective;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE function TestBoxLogic_addGroupEntry(a_uidAuthor INTEGER,
+ a_idTestBox INTEGER,
+ a_idSchedGroup INTEGER,
+ a_iSchedPriority INTEGER,
+ OUT r_tsEffective TIMESTAMP WITH TIME ZONE
+ ) AS $$
+ BEGIN
+ PERFORM TestBoxLogic_checkSchedGroupExists(a_idSchedGroup);
+ PERFORM TestBoxLogic_checkTestBoxNotInSchedGroup(a_idTestBox, a_idSchedGroup);
+
+ INSERT INTO TestBoxesInSchedGroups (
+ idTestBox,
+ idSchedGroup,
+ tsEffective,
+ tsExpire,
+ uidAuthor,
+ iSchedPriority)
+ VALUES (a_idTestBox,
+ a_idSchedGroup,
+ CURRENT_TIMESTAMP,
+ 'infinity'::TIMESTAMP,
+ a_uidAuthor,
+ a_iSchedPriority)
+ RETURNING tsEffective INTO r_tsEffective;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Only adds the user settable parts of the row, i.e. not what TestBoxLogic_updateOnSignOn touches.
+--
+CREATE OR REPLACE function TestBoxLogic_editEntry(a_uidAuthor INTEGER,
+ a_idTestBox INTEGER,
+ a_ip inet,
+ a_uuidSystem uuid,
+ a_sName TEXT,
+ a_sDescription TEXT,
+ a_fEnabled BOOLEAN,
+ a_enmLomKind LomKind_T,
+ a_ipLom inet,
+ a_pctScaleTimeout INTEGER, -- Actually smallint, but default typing fun.
+ a_sComment TEXT,
+ a_enmPendingCmd TestBoxCmd_T,
+ OUT r_idGenTestBox INTEGER,
+ OUT r_tsEffective TIMESTAMP WITH TIME ZONE
+ ) AS $$
+ DECLARE
+ v_Row TestBoxes%ROWTYPE;
+ v_idStrDescription INTEGER;
+ v_idStrComment INTEGER;
+ BEGIN
+ PERFORM TestBoxLogic_checkUniqueName(a_sName, a_idTestBox);
+
+ SELECT TestBoxLogic_lookupOrFindString(a_sDescription) INTO v_idStrDescription;
+ SELECT TestBoxLogic_lookupOrFindString(a_sComment) INTO v_idStrComment;
+
+ -- Fetch and historize the current row - there must be one.
+ UPDATE TestBoxes
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestBox = a_idTestBox
+ AND tsExpire = 'infinity'::TIMESTAMP
+ RETURNING * INTO STRICT v_Row;
+
+ -- Modify the row with the new data.
+ v_Row.uidAuthor := a_uidAuthor;
+ v_Row.ip := a_ip;
+ v_Row.uuidSystem := a_uuidSystem;
+ v_Row.sName := a_sName;
+ v_Row.idStrDescription := v_idStrDescription;
+ v_Row.fEnabled := a_fEnabled;
+ v_Row.enmLomKind := a_enmLomKind;
+ v_Row.ipLom := a_ipLom;
+ v_Row.pctScaleTimeout := a_pctScaleTimeout;
+ v_Row.idStrComment := v_idStrComment;
+ v_Row.enmPendingCmd := a_enmPendingCmd;
+ v_Row.tsEffective := v_Row.tsExpire;
+ r_tsEffective := v_Row.tsExpire;
+ v_Row.tsExpire := 'infinity'::TIMESTAMP;
+
+ -- Get a new generation ID.
+ SELECT NEXTVAL('TestBoxGenIdSeq') INTO v_Row.idGenTestBox;
+ r_idGenTestBox := v_Row.idGenTestBox;
+
+ -- Insert the modified row.
+ INSERT INTO TestBoxes VALUES (v_Row.*);
+ END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE function TestBoxLogic_editGroupEntry(a_uidAuthor INTEGER,
+ a_idTestBox INTEGER,
+ a_idSchedGroup INTEGER,
+ a_iSchedPriority INTEGER,
+ OUT r_tsEffective TIMESTAMP WITH TIME ZONE
+ ) AS $$
+ DECLARE
+ v_Row TestBoxesInSchedGroups%ROWTYPE;
+ v_idStrDescription INTEGER;
+ v_idStrComment INTEGER;
+ BEGIN
+ PERFORM TestBoxLogic_checkSchedGroupExists(a_idSchedGroup);
+
+ -- Fetch and historize the current row - there must be one.
+ UPDATE TestBoxesInSchedGroups
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestBox = a_idTestBox
+ AND idSchedGroup = a_idSchedGroup
+ AND tsExpire = 'infinity'::TIMESTAMP
+ RETURNING * INTO STRICT v_Row;
+
+ -- Modify the row with the new data.
+ v_Row.uidAuthor := a_uidAuthor;
+ v_Row.iSchedPriority := a_iSchedPriority;
+ v_Row.tsEffective := v_Row.tsExpire;
+ r_tsEffective := v_Row.tsExpire;
+ v_Row.tsExpire := 'infinity'::TIMESTAMP;
+
+ -- Insert the modified row.
+ INSERT INTO TestBoxesInSchedGroups VALUES (v_Row.*);
+ END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION TestBoxLogic_removeEntry(a_uidAuthor INTEGER, a_idTestBox INTEGER, a_fCascade BOOLEAN)
+ RETURNS VOID AS $$
+ DECLARE
+ v_Row TestBoxes%ROWTYPE;
+ v_tsEffective TIMESTAMP WITH TIME ZONE;
+ v_Rec RECORD;
+ v_sErrors TEXT;
+ BEGIN
+ --
+ -- Check preconditions.
+ --
+ IF a_fCascade <> TRUE THEN
+ -- @todo implement checks which throws useful exceptions.
+ ELSE
+ RAISE EXCEPTION 'CASCADE test box deletion is not implemented';
+ END IF;
+
+ --
+ -- Delete all current groups, skipping history since we're also deleting the testbox.
+ --
+ UPDATE TestBoxesInSchedGroups
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestBox = a_idTestBox
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ --
+ -- To preserve the information about who deleted the record, we try to
+ -- add a dummy record which expires immediately. I say try because of
+ -- the primary key, we must let the new record be valid for 1 us. :-(
+ --
+ SELECT * INTO STRICT v_Row
+ FROM TestBoxes
+ WHERE idTestBox = a_idTestBox
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ v_tsEffective := CURRENT_TIMESTAMP - INTERVAL '1 microsecond';
+ IF v_Row.tsEffective < v_tsEffective THEN
+ PERFORM TestBoxLogic_historizeEntry(v_Row.idGenTestBox, v_tsEffective);
+
+ v_Row.tsEffective := v_tsEffective;
+ v_Row.tsExpire := CURRENT_TIMESTAMP;
+ v_Row.uidAuthor := a_uidAuthor;
+ SELECT NEXTVAL('TestBoxGenIdSeq') INTO v_Row.idGenTestBox;
+ INSERT INTO TestBoxes VALUES (v_Row.*);
+ ELSE
+ PERFORM TestBoxLogic_historizeEntry(v_Row.idGenTestBox, CURRENT_TIMESTAMP);
+ END IF;
+
+ EXCEPTION
+ WHEN NO_DATA_FOUND THEN
+ RAISE EXCEPTION 'Test box with ID % does not currently exist', a_idTestBox;
+ WHEN TOO_MANY_ROWS THEN
+ RAISE EXCEPTION 'Integrity error in TestBoxes: Too many current rows for %', a_idTestBox;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION TestBoxLogic_removeGroupEntry(a_uidAuthor INTEGER, a_idTestBox INTEGER, a_idSchedGroup INTEGER)
+ RETURNS VOID AS $$
+ DECLARE
+ v_Row TestBoxesInSchedGroups%ROWTYPE;
+ v_tsEffective TIMESTAMP WITH TIME ZONE;
+ BEGIN
+ --
+ -- To preserve the information about who deleted the record, we try to
+ -- add a dummy record which expires immediately. I say try because of
+ -- the primary key, we must let the new record be valid for 1 us. :-(
+ --
+ SELECT * INTO STRICT v_Row
+ FROM TestBoxesInSchedGroups
+ WHERE idTestBox = a_idTestBox
+ AND idSchedGroup = a_idSchedGroup
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ v_tsEffective := CURRENT_TIMESTAMP - INTERVAL '1 microsecond';
+ IF v_Row.tsEffective < v_tsEffective THEN
+ PERFORM TestBoxLogic_historizeGroupEntry(a_idTestBox, a_idSchedGroup, v_tsEffective);
+
+ v_Row.tsEffective := v_tsEffective;
+ v_Row.tsExpire := CURRENT_TIMESTAMP;
+ v_Row.uidAuthor := a_uidAuthor;
+ INSERT INTO TestBoxesInSchedGroups VALUES (v_Row.*);
+ ELSE
+ PERFORM TestBoxLogic_historizeGroupEntry(a_idTestBox, a_idSchedGroup, CURRENT_TIMESTAMP);
+ END IF;
+
+ EXCEPTION
+ WHEN NO_DATA_FOUND THEN
+ RAISE EXCEPTION 'TestBox #% does is not currently a member of scheduling group #%', a_idTestBox, a_idSchedGroup;
+ WHEN TOO_MANY_ROWS THEN
+ RAISE EXCEPTION 'Integrity error in TestBoxesInSchedGroups: Too many current rows for % / %',
+ a_idTestBox, a_idSchedGroup;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Sign on update
+--
+CREATE OR REPLACE function TestBoxLogic_updateOnSignOn(a_idTestBox INTEGER,
+ a_ip inet,
+ a_sOs TEXT,
+ a_sOsVersion TEXT,
+ a_sCpuVendor TEXT,
+ a_sCpuArch TEXT,
+ a_sCpuName TEXT,
+ a_lCpuRevision bigint,
+ a_cCpus INTEGER, -- Actually smallint, but default typing fun.
+ a_fCpuHwVirt boolean,
+ a_fCpuNestedPaging boolean,
+ a_fCpu64BitGuest boolean,
+ a_fChipsetIoMmu boolean,
+ a_fRawMode boolean,
+ a_cMbMemory bigint,
+ a_cMbScratch bigint,
+ a_sReport TEXT,
+ a_iTestBoxScriptRev INTEGER,
+ a_iPythonHexVersion INTEGER,
+ OUT r_idGenTestBox INTEGER
+ ) AS $$
+ DECLARE
+ v_Row TestBoxes%ROWTYPE;
+ v_idStrOs INTEGER;
+ v_idStrOsVersion INTEGER;
+ v_idStrCpuVendor INTEGER;
+ v_idStrCpuArch INTEGER;
+ v_idStrCpuName INTEGER;
+ v_idStrReport INTEGER;
+ BEGIN
+ SELECT TestBoxLogic_lookupOrFindString(a_sOs) INTO v_idStrOs;
+ SELECT TestBoxLogic_lookupOrFindString(a_sOsVersion) INTO v_idStrOsVersion;
+ SELECT TestBoxLogic_lookupOrFindString(a_sCpuVendor) INTO v_idStrCpuVendor;
+ SELECT TestBoxLogic_lookupOrFindString(a_sCpuArch) INTO v_idStrCpuArch;
+ SELECT TestBoxLogic_lookupOrFindString(a_sCpuName) INTO v_idStrCpuName;
+ SELECT TestBoxLogic_lookupOrFindString(a_sReport) INTO v_idStrReport;
+
+ -- Fetch and historize the current row - there must be one.
+ UPDATE TestBoxes
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestBox = a_idTestBox
+ AND tsExpire = 'infinity'::TIMESTAMP
+ RETURNING * INTO STRICT v_Row;
+
+ -- Modify the row with the new data.
+ v_Row.uidAuthor := NULL;
+ v_Row.ip := a_ip;
+ v_Row.idStrOs := v_idStrOs;
+ v_Row.idStrOsVersion := v_idStrOsVersion;
+ v_Row.idStrCpuVendor := v_idStrCpuVendor;
+ v_Row.idStrCpuArch := v_idStrCpuArch;
+ v_Row.idStrCpuName := v_idStrCpuName;
+ v_Row.lCpuRevision := a_lCpuRevision;
+ v_Row.cCpus := a_cCpus;
+ v_Row.fCpuHwVirt := a_fCpuHwVirt;
+ v_Row.fCpuNestedPaging := a_fCpuNestedPaging;
+ v_Row.fCpu64BitGuest := a_fCpu64BitGuest;
+ v_Row.fChipsetIoMmu := a_fChipsetIoMmu;
+ v_Row.fRawMode := a_fRawMode;
+ v_Row.cMbMemory := a_cMbMemory;
+ v_Row.cMbScratch := a_cMbScratch;
+ v_Row.idStrReport := v_idStrReport;
+ v_Row.iTestBoxScriptRev := a_iTestBoxScriptRev;
+ v_Row.iPythonHexVersion := a_iPythonHexVersion;
+ v_Row.tsEffective := v_Row.tsExpire;
+ v_Row.tsExpire := 'infinity'::TIMESTAMP;
+
+ -- Get a new generation ID.
+ SELECT NEXTVAL('TestBoxGenIdSeq') INTO v_Row.idGenTestBox;
+ r_idGenTestBox := v_Row.idGenTestBox;
+
+ -- Insert the modified row.
+ INSERT INTO TestBoxes VALUES (v_Row.*);
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Set new command.
+--
+CREATE OR REPLACE function TestBoxLogic_setCommand(a_uidAuthor INTEGER,
+ a_idTestBox INTEGER,
+ a_enmOldCmd TestBoxCmd_T,
+ a_enmNewCmd TestBoxCmd_T,
+ a_sComment TEXT,
+ OUT r_idGenTestBox INTEGER,
+ OUT r_tsEffective TIMESTAMP WITH TIME ZONE
+ ) AS $$
+ DECLARE
+ v_Row TestBoxes%ROWTYPE;
+ v_idStrComment INTEGER;
+ BEGIN
+ SELECT TestBoxLogic_lookupOrFindString(a_sComment) INTO v_idStrComment;
+
+ -- Fetch and historize the current row - there must be one.
+ UPDATE TestBoxes
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestBox = a_idTestBox
+ AND tsExpire = 'infinity'::TIMESTAMP
+ AND enmPendingCmd = a_enmOldCmd
+ RETURNING * INTO STRICT v_Row;
+
+ -- Modify the row with the new data.
+ v_Row.enmPendingCmd := a_enmNewCmd;
+ IF v_idStrComment IS NOT NULL THEN
+ v_Row.idStrComment := v_idStrComment;
+ END IF;
+ v_Row.tsEffective := v_Row.tsExpire;
+ r_tsEffective := v_Row.tsExpire;
+ v_Row.tsExpire := 'infinity'::TIMESTAMP;
+
+ -- Get a new generation ID.
+ SELECT NEXTVAL('TestBoxGenIdSeq') INTO v_Row.idGenTestBox;
+ r_idGenTestBox := v_Row.idGenTestBox;
+
+ -- Insert the modified row.
+ INSERT INTO TestBoxes VALUES (v_Row.*);
+ END;
+$$ LANGUAGE plpgsql;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testbox.py b/src/VBox/ValidationKit/testmanager/core/testbox.py
new file mode 100755
index 00000000..6686ca3b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testbox.py
@@ -0,0 +1,1286 @@
+# -*- coding: utf-8 -*-
+# $Id: testbox.py $
+
+"""
+Test Manager - TestBox.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import copy;
+import sys;
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core import db;
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMInFligthCollision, \
+ TMInvalidData, TMTooManyRows, TMRowNotFound, \
+ ChangeLogEntry, AttributeChangeEntry, AttributeChangeEntryPre;
+from testmanager.core.useraccount import UserAccountLogic;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ xrange = range; # pylint: disable=redefined-builtin,invalid-name
+
+
+class TestBoxInSchedGroupData(ModelDataBase):
+ """
+ TestBox in SchedGroup data.
+ """
+
+ ksParam_idTestBox = 'TestBoxInSchedGroup_idTestBox';
+ ksParam_idSchedGroup = 'TestBoxInSchedGroup_idSchedGroup';
+ ksParam_tsEffective = 'TestBoxInSchedGroup_tsEffective';
+ ksParam_tsExpire = 'TestBoxInSchedGroup_tsExpire';
+ ksParam_uidAuthor = 'TestBoxInSchedGroup_uidAuthor';
+ ksParam_iSchedPriority = 'TestBoxInSchedGroup_iSchedPriority';
+
+ kasAllowNullAttributes = [ 'tsEffective', 'tsExpire', 'uidAuthor', ]
+
+ kiMin_iSchedPriority = 0;
+ kiMax_iSchedPriority = 32;
+
+ kcDbColumns = 6;
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+ self.idTestBox = None;
+ self.idSchedGroup = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.iSchedPriority = 16;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Expecting the result from a query like this:
+ SELECT * FROM TestBoxesInSchedGroups
+ """
+ if aoRow is None:
+ raise TMRowNotFound('TestBox/SchedGroup not found.');
+
+ self.idTestBox = aoRow[0];
+ self.idSchedGroup = aoRow[1];
+ self.tsEffective = aoRow[2];
+ self.tsExpire = aoRow[3];
+ self.uidAuthor = aoRow[4];
+ self.iSchedPriority = aoRow[5];
+
+ return self;
+
+class TestBoxInSchedGroupDataEx(TestBoxInSchedGroupData):
+ """
+ Extended version of TestBoxInSchedGroupData that contains the scheduling group.
+ """
+
+ def __init__(self):
+ TestBoxInSchedGroupData.__init__(self);
+ self.oSchedGroup = None # type: SchedGroupData
+
+ def initFromDbRowEx(self, aoRow, oDb, tsNow = None, sPeriodBack = None):
+ """
+ Extended version of initFromDbRow that fills in the rest from the database.
+ """
+ from testmanager.core.schedgroup import SchedGroupData;
+ self.initFromDbRow(aoRow);
+ self.oSchedGroup = SchedGroupData().initFromDbWithId(oDb, self.idSchedGroup, tsNow, sPeriodBack);
+ return self;
+
+class TestBoxDataForSchedGroup(TestBoxInSchedGroupData):
+ """
+ Extended version of TestBoxInSchedGroupData that adds the testbox data (if available).
+ Used by TestBoxLogic.fetchForSchedGroup
+ """
+
+ def __init__(self):
+ TestBoxInSchedGroupData.__init__(self);
+ self.oTestBox = None # type: TestBoxData
+
+ def initFromDbRow(self, aoRow):
+ """
+ The row is: TestBoxesInSchedGroups.*, TestBoxesWithStrings.*
+ """
+ TestBoxInSchedGroupData.initFromDbRow(self, aoRow);
+ if aoRow[self.kcDbColumns]:
+ self.oTestBox = TestBoxData().initFromDbRow(aoRow[self.kcDbColumns:]);
+ else:
+ self.oTestBox = None;
+ return self;
+
+ def getDataAttributes(self):
+ asAttributes = TestBoxInSchedGroupData.getDataAttributes(self);
+ asAttributes.remove('oTestBox');
+ return asAttributes;
+
+ def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other):
+ dErrors = TestBoxInSchedGroupData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor);
+ if self.ksParam_idTestBox not in dErrors:
+ self.oTestBox = TestBoxData();
+ try:
+ self.oTestBox.initFromDbWithId(oDb, self.idTestBox);
+ except Exception as oXcpt:
+ self.oTestBox = TestBoxData()
+ dErrors[self.ksParam_idTestBox] = str(oXcpt);
+ return dErrors;
+
+
+# pylint: disable=invalid-name
+class TestBoxData(ModelDataBase): # pylint: disable=too-many-instance-attributes
+ """
+ TestBox Data.
+ """
+
+ ## LomKind_T
+ ksLomKind_None = 'none';
+ ksLomKind_ILOM = 'ilom';
+ ksLomKind_ELOM = 'elom';
+ ksLomKind_AppleXserveLom = 'apple-xserver-lom';
+ kasLomKindValues = [ ksLomKind_None, ksLomKind_ILOM, ksLomKind_ELOM, ksLomKind_AppleXserveLom];
+ kaoLomKindDescs = \
+ [
+ ( ksLomKind_None, 'None', ''),
+ ( ksLomKind_ILOM, 'ILOM', ''),
+ ( ksLomKind_ELOM, 'ELOM', ''),
+ ( ksLomKind_AppleXserveLom, 'Apple Xserve LOM', ''),
+ ];
+
+
+ ## TestBoxCmd_T
+ ksTestBoxCmd_None = 'none';
+ ksTestBoxCmd_Abort = 'abort';
+ ksTestBoxCmd_Reboot = 'reboot';
+ ksTestBoxCmd_Upgrade = 'upgrade';
+ ksTestBoxCmd_UpgradeAndReboot = 'upgrade-and-reboot';
+ ksTestBoxCmd_Special = 'special';
+ kasTestBoxCmdValues = [ ksTestBoxCmd_None, ksTestBoxCmd_Abort, ksTestBoxCmd_Reboot, ksTestBoxCmd_Upgrade,
+ ksTestBoxCmd_UpgradeAndReboot, ksTestBoxCmd_Special];
+ kaoTestBoxCmdDescs = \
+ [
+ ( ksTestBoxCmd_None, 'None', ''),
+ ( ksTestBoxCmd_Abort, 'Abort current test', ''),
+ ( ksTestBoxCmd_Reboot, 'Reboot TestBox', ''),
+ ( ksTestBoxCmd_Upgrade, 'Upgrade TestBox Script', ''),
+ ( ksTestBoxCmd_UpgradeAndReboot, 'Upgrade TestBox Script and reboot', ''),
+ ( ksTestBoxCmd_Special, 'Special (reserved)', ''),
+ ];
+
+
+ ksIdAttr = 'idTestBox';
+ ksIdGenAttr = 'idGenTestBox';
+
+ ksParam_idTestBox = 'TestBox_idTestBox';
+ ksParam_tsEffective = 'TestBox_tsEffective';
+ ksParam_tsExpire = 'TestBox_tsExpire';
+ ksParam_uidAuthor = 'TestBox_uidAuthor';
+ ksParam_idGenTestBox = 'TestBox_idGenTestBox';
+ ksParam_ip = 'TestBox_ip';
+ ksParam_uuidSystem = 'TestBox_uuidSystem';
+ ksParam_sName = 'TestBox_sName';
+ ksParam_sDescription = 'TestBox_sDescription';
+ ksParam_fEnabled = 'TestBox_fEnabled';
+ ksParam_enmLomKind = 'TestBox_enmLomKind';
+ ksParam_ipLom = 'TestBox_ipLom';
+ ksParam_pctScaleTimeout = 'TestBox_pctScaleTimeout';
+ ksParam_sComment = 'TestBox_sComment';
+ ksParam_sOs = 'TestBox_sOs';
+ ksParam_sOsVersion = 'TestBox_sOsVersion';
+ ksParam_sCpuVendor = 'TestBox_sCpuVendor';
+ ksParam_sCpuArch = 'TestBox_sCpuArch';
+ ksParam_sCpuName = 'TestBox_sCpuName';
+ ksParam_lCpuRevision = 'TestBox_lCpuRevision';
+ ksParam_cCpus = 'TestBox_cCpus';
+ ksParam_fCpuHwVirt = 'TestBox_fCpuHwVirt';
+ ksParam_fCpuNestedPaging = 'TestBox_fCpuNestedPaging';
+ ksParam_fCpu64BitGuest = 'TestBox_fCpu64BitGuest';
+ ksParam_fChipsetIoMmu = 'TestBox_fChipsetIoMmu';
+ ksParam_fRawMode = 'TestBox_fRawMode';
+ ksParam_cMbMemory = 'TestBox_cMbMemory';
+ ksParam_cMbScratch = 'TestBox_cMbScratch';
+ ksParam_sReport = 'TestBox_sReport';
+ ksParam_iTestBoxScriptRev = 'TestBox_iTestBoxScriptRev';
+ ksParam_iPythonHexVersion = 'TestBox_iPythonHexVersion';
+ ksParam_enmPendingCmd = 'TestBox_enmPendingCmd';
+
+ kasInternalAttributes = [ 'idStrDescription', 'idStrComment', 'idStrOs', 'idStrOsVersion', 'idStrCpuVendor',
+ 'idStrCpuArch', 'idStrCpuName', 'idStrReport', ];
+ kasMachineSettableOnly = [ 'sOs', 'sOsVersion', 'sCpuVendor', 'sCpuArch', 'sCpuName', 'lCpuRevision', 'cCpus',
+ 'fCpuHwVirt', 'fCpuNestedPaging', 'fCpu64BitGuest', 'fChipsetIoMmu', 'fRawMode',
+ 'cMbMemory', 'cMbScratch', 'sReport', 'iTestBoxScriptRev', 'iPythonHexVersion', ];
+ kasAllowNullAttributes = ['idTestBox', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestBox', 'sDescription',
+ 'ipLom', 'sComment', ] + kasMachineSettableOnly + kasInternalAttributes;
+
+ kasValidValues_enmLomKind = kasLomKindValues;
+ kasValidValues_enmPendingCmd = kasTestBoxCmdValues;
+ kiMin_pctScaleTimeout = 11;
+ kiMax_pctScaleTimeout = 19999;
+ kcchMax_sReport = 65535;
+
+ kcDbColumns = 40; # including the 7 string joins columns
+
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestBox = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.idGenTestBox = None;
+ self.ip = None;
+ self.uuidSystem = None;
+ self.sName = None;
+ self.idStrDescription = None;
+ self.fEnabled = False;
+ self.enmLomKind = self.ksLomKind_None;
+ self.ipLom = None;
+ self.pctScaleTimeout = 100;
+ self.idStrComment = None;
+ self.idStrOs = None;
+ self.idStrOsVersion = None;
+ self.idStrCpuVendor = None;
+ self.idStrCpuArch = None;
+ self.idStrCpuName = None;
+ self.lCpuRevision = None;
+ self.cCpus = 1;
+ self.fCpuHwVirt = False;
+ self.fCpuNestedPaging = False;
+ self.fCpu64BitGuest = False;
+ self.fChipsetIoMmu = False;
+ self.fRawMode = None;
+ self.cMbMemory = 1;
+ self.cMbScratch = 0;
+ self.idStrReport = None;
+ self.iTestBoxScriptRev = 0;
+ self.iPythonHexVersion = 0;
+ self.enmPendingCmd = self.ksTestBoxCmd_None;
+ # String table values.
+ self.sDescription = None;
+ self.sComment = None;
+ self.sOs = None;
+ self.sOsVersion = None;
+ self.sCpuVendor = None;
+ self.sCpuArch = None;
+ self.sCpuName = None;
+ self.sReport = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Internal worker for initFromDbWithId and initFromDbWithGenId as well as
+ from TestBoxLogic. Expecting the result from a query like this:
+ SELECT TestBoxesWithStrings.* FROM TestBoxesWithStrings
+ """
+ if aoRow is None:
+ raise TMRowNotFound('TestBox not found.');
+
+ self.idTestBox = aoRow[0];
+ self.tsEffective = aoRow[1];
+ self.tsExpire = aoRow[2];
+ self.uidAuthor = aoRow[3];
+ self.idGenTestBox = aoRow[4];
+ self.ip = aoRow[5];
+ self.uuidSystem = aoRow[6];
+ self.sName = aoRow[7];
+ self.idStrDescription = aoRow[8];
+ self.fEnabled = aoRow[9];
+ self.enmLomKind = aoRow[10];
+ self.ipLom = aoRow[11];
+ self.pctScaleTimeout = aoRow[12];
+ self.idStrComment = aoRow[13];
+ self.idStrOs = aoRow[14];
+ self.idStrOsVersion = aoRow[15];
+ self.idStrCpuVendor = aoRow[16];
+ self.idStrCpuArch = aoRow[17];
+ self.idStrCpuName = aoRow[18];
+ self.lCpuRevision = aoRow[19];
+ self.cCpus = aoRow[20];
+ self.fCpuHwVirt = aoRow[21];
+ self.fCpuNestedPaging = aoRow[22];
+ self.fCpu64BitGuest = aoRow[23];
+ self.fChipsetIoMmu = aoRow[24];
+ self.fRawMode = aoRow[25];
+ self.cMbMemory = aoRow[26];
+ self.cMbScratch = aoRow[27];
+ self.idStrReport = aoRow[28];
+ self.iTestBoxScriptRev = aoRow[29];
+ self.iPythonHexVersion = aoRow[30];
+ self.enmPendingCmd = aoRow[31];
+
+ # String table values.
+ if len(aoRow) > 32:
+ self.sDescription = aoRow[32];
+ self.sComment = aoRow[33];
+ self.sOs = aoRow[34];
+ self.sOsVersion = aoRow[35];
+ self.sCpuVendor = aoRow[36];
+ self.sCpuArch = aoRow[37];
+ self.sCpuName = aoRow[38];
+ self.sReport = aoRow[39];
+
+ return self;
+
+ def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT TestBoxesWithStrings.*\n'
+ 'FROM TestBoxesWithStrings\n'
+ 'WHERE idTestBox = %s\n'
+ , ( idTestBox, ), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idTestBox=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestBox, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+ def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None):
+ """
+ Initialize the object from the database.
+ """
+ _ = tsNow; # Only useful for extended data classes.
+ oDb.execute('SELECT TestBoxesWithStrings.*\n'
+ 'FROM TestBoxesWithStrings\n'
+ 'WHERE idGenTestBox = %s\n'
+ , (idGenTestBox, ) );
+ return self.initFromDbRow(oDb.fetchOne());
+
+ def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other):
+ # Override to do extra ipLom checks.
+ dErrors = ModelDataBase._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor);
+ if self.ksParam_ipLom not in dErrors \
+ and self.ksParam_enmLomKind not in dErrors \
+ and self.enmLomKind != self.ksLomKind_None \
+ and self.ipLom is None:
+ dErrors[self.ksParam_ipLom] = 'Light-out-management IP is mandatory and a LOM is selected.'
+ return dErrors;
+
+ @staticmethod
+ def formatPythonVersionEx(iPythonHexVersion):
+ """ Unbuttons the version number and formats it as a version string. """
+ if iPythonHexVersion is None:
+ return 'N/A';
+ return 'v%d.%d.%d.%d' \
+ % ( iPythonHexVersion >> 24,
+ (iPythonHexVersion >> 16) & 0xff,
+ (iPythonHexVersion >> 8) & 0xff,
+ iPythonHexVersion & 0xff);
+
+ def formatPythonVersion(self):
+ """ Unbuttons the version number and formats it as a version string. """
+ return self.formatPythonVersionEx(self.iPythonHexVersion);
+
+
+ @staticmethod
+ def getCpuFamilyEx(lCpuRevision):
+ """ Returns the CPU family for a x86 or amd64 testboxes."""
+ if lCpuRevision is None:
+ return 0;
+ return (lCpuRevision >> 24 & 0xff);
+
+ def getCpuFamily(self):
+ """ Returns the CPU family for a x86 or amd64 testboxes."""
+ return self.getCpuFamilyEx(self.lCpuRevision);
+
+ @staticmethod
+ def getCpuModelEx(lCpuRevision):
+ """ Returns the CPU model for a x86 or amd64 testboxes."""
+ if lCpuRevision is None:
+ return 0;
+ return (lCpuRevision >> 8 & 0xffff);
+
+ def getCpuModel(self):
+ """ Returns the CPU model for a x86 or amd64 testboxes."""
+ return self.getCpuModelEx(self.lCpuRevision);
+
+ @staticmethod
+ def getCpuSteppingEx(lCpuRevision):
+ """ Returns the CPU stepping for a x86 or amd64 testboxes."""
+ if lCpuRevision is None:
+ return 0;
+ return (lCpuRevision & 0xff);
+
+ def getCpuStepping(self):
+ """ Returns the CPU stepping for a x86 or amd64 testboxes."""
+ return self.getCpuSteppingEx(self.lCpuRevision);
+
+
+ # The following is a translation of the g_aenmIntelFamily06 array in CPUMR3CpuId.cpp:
+ kdIntelFamily06 = {
+ 0x00: 'P6',
+ 0x01: 'P6',
+ 0x03: 'P6_II',
+ 0x05: 'P6_II',
+ 0x06: 'P6_II',
+ 0x07: 'P6_III',
+ 0x08: 'P6_III',
+ 0x09: 'P6_M_Banias',
+ 0x0a: 'P6_III',
+ 0x0b: 'P6_III',
+ 0x0d: 'P6_M_Dothan',
+ 0x0e: 'Core_Yonah',
+ 0x0f: 'Core2_Merom',
+ 0x15: 'P6_M_Dothan',
+ 0x16: 'Core2_Merom',
+ 0x17: 'Core2_Penryn',
+ 0x1a: 'Core7_Nehalem',
+ 0x1c: 'Atom_Bonnell',
+ 0x1d: 'Core2_Penryn',
+ 0x1e: 'Core7_Nehalem',
+ 0x1f: 'Core7_Nehalem',
+ 0x25: 'Core7_Westmere',
+ 0x26: 'Atom_Lincroft',
+ 0x27: 'Atom_Saltwell',
+ 0x2a: 'Core7_SandyBridge',
+ 0x2c: 'Core7_Westmere',
+ 0x2d: 'Core7_SandyBridge',
+ 0x2e: 'Core7_Nehalem',
+ 0x2f: 'Core7_Westmere',
+ 0x35: 'Atom_Saltwell',
+ 0x36: 'Atom_Saltwell',
+ 0x37: 'Atom_Silvermont',
+ 0x3a: 'Core7_IvyBridge',
+ 0x3c: 'Core7_Haswell',
+ 0x3d: 'Core7_Broadwell',
+ 0x3e: 'Core7_IvyBridge',
+ 0x3f: 'Core7_Haswell',
+ 0x45: 'Core7_Haswell',
+ 0x46: 'Core7_Haswell',
+ 0x47: 'Core7_Broadwell',
+ 0x4a: 'Atom_Silvermont',
+ 0x4c: 'Atom_Airmount',
+ 0x4d: 'Atom_Silvermont',
+ 0x4e: 'Core7_Skylake',
+ 0x4f: 'Core7_Broadwell',
+ 0x55: 'Core7_Skylake',
+ 0x56: 'Core7_Broadwell',
+ 0x5a: 'Atom_Silvermont',
+ 0x5c: 'Atom_Goldmont',
+ 0x5d: 'Atom_Silvermont',
+ 0x5e: 'Core7_Skylake',
+ 0x66: 'Core7_Cannonlake',
+ };
+ # Also from CPUMR3CpuId.cpp, but the switch.
+ kdIntelFamily15 = {
+ 0x00: 'NB_Willamette',
+ 0x01: 'NB_Willamette',
+ 0x02: 'NB_Northwood',
+ 0x03: 'NB_Prescott',
+ 0x04: 'NB_Prescott2M',
+ 0x05: 'NB_Unknown',
+ 0x06: 'NB_CedarMill',
+ 0x07: 'NB_Gallatin',
+ };
+
+ @staticmethod
+ def queryCpuMicroarchEx(lCpuRevision, sCpuVendor):
+ """ Try guess the microarch name for the cpu. Returns None if we cannot. """
+ if lCpuRevision is None or sCpuVendor is None:
+ return None;
+ uFam = TestBoxData.getCpuFamilyEx(lCpuRevision);
+ uMod = TestBoxData.getCpuModelEx(lCpuRevision);
+ if sCpuVendor == 'GenuineIntel':
+ if uFam == 6:
+ return TestBoxData.kdIntelFamily06.get(uMod, None);
+ if uFam == 15:
+ return TestBoxData.kdIntelFamily15.get(uMod, None);
+ elif sCpuVendor == 'AuthenticAMD':
+ if uFam == 0xf:
+ if uMod < 0x10: return 'K8_130nm';
+ if 0x60 <= uMod < 0x80: return 'K8_65nm';
+ if uMod >= 0x40: return 'K8_90nm_AMDV';
+ if uMod in [0x21, 0x23, 0x2b, 0x37, 0x3f]: return 'K8_90nm_DualCore';
+ return 'AMD_K8_90nm';
+ if uFam == 0x10: return 'K10';
+ if uFam == 0x11: return 'K10_Lion';
+ if uFam == 0x12: return 'K10_Llano';
+ if uFam == 0x14: return 'Bobcat';
+ if uFam == 0x15:
+ if uMod <= 0x01: return 'Bulldozer';
+ if uMod in [0x02, 0x10, 0x13]: return 'Piledriver';
+ return None;
+ if uFam == 0x16:
+ return 'Jaguar';
+ elif sCpuVendor == 'CentaurHauls':
+ if uFam == 0x05:
+ if uMod == 0x01: return 'Centaur_C6';
+ if uMod == 0x04: return 'Centaur_C6';
+ if uMod == 0x08: return 'Centaur_C2';
+ if uMod == 0x09: return 'Centaur_C3';
+ if uFam == 0x06:
+ if uMod == 0x05: return 'VIA_C3_M2';
+ if uMod == 0x06: return 'VIA_C3_C5A';
+ if uMod == 0x07: return 'VIA_C3_C5B' if TestBoxData.getCpuSteppingEx(lCpuRevision) < 8 else 'VIA_C3_C5C';
+ if uMod == 0x08: return 'VIA_C3_C5N';
+ if uMod == 0x09: return 'VIA_C3_C5XL' if TestBoxData.getCpuSteppingEx(lCpuRevision) < 8 else 'VIA_C3_C5P';
+ if uMod == 0x0a: return 'VIA_C7_C5J';
+ if uMod == 0x0f: return 'VIA_Isaiah';
+ elif sCpuVendor == ' Shanghai ':
+ if uFam == 0x07:
+ if uMod == 0x0b: return 'Shanghai_KX-5000';
+ return None;
+
+ def queryCpuMicroarch(self):
+ """ Try guess the microarch name for the cpu. Returns None if we cannot. """
+ return self.queryCpuMicroarchEx(self.lCpuRevision, self.sCpuVendor);
+
+ @staticmethod
+ def getPrettyCpuVersionEx(lCpuRevision, sCpuVendor):
+ """ Pretty formatting of the family/model/stepping with microarch optimizations. """
+ if lCpuRevision is None or sCpuVendor is None:
+ return u'<none>';
+ sMarch = TestBoxData.queryCpuMicroarchEx(lCpuRevision, sCpuVendor);
+ if sMarch is not None:
+ return '%s %02x:%x' \
+ % (sMarch, TestBoxData.getCpuModelEx(lCpuRevision), TestBoxData.getCpuSteppingEx(lCpuRevision));
+ return 'fam%02X m%02X s%02X' \
+ % ( TestBoxData.getCpuFamilyEx(lCpuRevision), TestBoxData.getCpuModelEx(lCpuRevision),
+ TestBoxData.getCpuSteppingEx(lCpuRevision));
+
+ def getPrettyCpuVersion(self):
+ """ Pretty formatting of the family/model/stepping with microarch optimizations. """
+ return self.getPrettyCpuVersionEx(self.lCpuRevision, self.sCpuVendor);
+
+ def getArchBitString(self):
+ """ Returns 32-bit, 64-bit, <none>, or sCpuArch. """
+ if self.sCpuArch is None:
+ return '<none>';
+ if self.sCpuArch in [ 'x86',]:
+ return '32-bit';
+ if self.sCpuArch in [ 'amd64',]:
+ return '64-bit';
+ return self.sCpuArch;
+
+ def getPrettyCpuVendor(self):
+ """ Pretty vendor name."""
+ if self.sCpuVendor is None:
+ return '<none>';
+ if self.sCpuVendor == 'GenuineIntel': return 'Intel';
+ if self.sCpuVendor == 'AuthenticAMD': return 'AMD';
+ if self.sCpuVendor == 'CentaurHauls': return 'VIA';
+ if self.sCpuVendor == ' Shanghai ': return 'Shanghai';
+ return self.sCpuVendor;
+
+
+class TestBoxDataEx(TestBoxData):
+ """
+ TestBox data.
+ """
+
+ ksParam_aoInSchedGroups = 'TestBox_aoInSchedGroups';
+
+ # Use [] instead of None.
+ kasAltArrayNull = [ 'aoInSchedGroups', ];
+
+ ## Helper parameter containing the comma separated list with the IDs of
+ # potential members found in the parameters.
+ ksParam_aidSchedGroups = 'TestBoxDataEx_aidSchedGroups';
+
+ def __init__(self):
+ TestBoxData.__init__(self);
+ self.aoInSchedGroups = [] # type: list[TestBoxInSchedGroupData]
+
+ def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None):
+ """
+ Worker shared by the initFromDb* methods.
+ Returns self. Raises exception if no row or database error.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM TestBoxesInSchedGroups\n'
+ 'WHERE idTestBox = %s\n'
+ , (self.idTestBox,), tsNow, sPeriodBack)
+ + 'ORDER BY idSchedGroup\n' );
+ self.aoInSchedGroups = [];
+ for aoRow in oDb.fetchAll():
+ self.aoInSchedGroups.append(TestBoxInSchedGroupDataEx().initFromDbRowEx(aoRow, oDb, tsNow, sPeriodBack));
+ return self;
+
+ def initFromDbRowEx(self, aoRow, oDb, tsNow = None):
+ """
+ Reinitialize from a SELECT * FROM TestBoxesWithStrings row. Will query the
+ necessary additional data from oDb using tsNow.
+ Returns self. Raises exception if no row or database error.
+ """
+ TestBoxData.initFromDbRow(self, aoRow);
+ return self._initExtraMembersFromDb(oDb, tsNow);
+
+ def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ TestBoxData.initFromDbWithId(self, oDb, idTestBox, tsNow, sPeriodBack);
+ return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack);
+
+ def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None):
+ """
+ Initialize the object from the database.
+ """
+ TestBoxData.initFromDbWithGenId(self, oDb, idGenTestBox);
+ if tsNow is None and not oDb.isTsInfinity(self.tsExpire):
+ tsNow = self.tsEffective;
+ return self._initExtraMembersFromDb(oDb, tsNow);
+
+ def getAttributeParamNullValues(self, sAttr): # Necessary?
+ if sAttr in ['aoInSchedGroups', ]:
+ return [[], ''];
+ return TestBoxData.getAttributeParamNullValues(self, sAttr);
+
+ def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
+ """
+ For dealing with the in-scheduling-group list.
+ """
+ if sAttr != 'aoInSchedGroups':
+ return TestBoxData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict);
+
+ aoNewValues = [];
+ aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = []);
+ asIds = oDisp.getStringParam(self.ksParam_aidSchedGroups, sDefault = '').split(',');
+ for idSchedGroup in asIds:
+ try: idSchedGroup = int(idSchedGroup);
+ except: pass;
+ oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestBoxDataEx.ksParam_aoInSchedGroups, idSchedGroup,))
+ oMember = TestBoxInSchedGroupData().initFromParams(oDispWrapper, fStrict = False);
+ if idSchedGroup in aidSelected:
+ aoNewValues.append(oMember);
+ return aoNewValues;
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): # pylint: disable=too-many-locals
+ """
+ Validate special arrays and requirement expressions.
+
+ Some special needs for the in-scheduling-group list.
+ """
+ if sAttr != 'aoInSchedGroups':
+ return TestBoxData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+
+ asErrors = [];
+ aoNewValues = [];
+
+ # Note! We'll be returning an error dictionary instead of an string here.
+ dErrors = {};
+
+ # HACK ALERT! idTestBox might not have been validated and converted yet, but we need detect
+ # adding so we can ignore idTestBox being NIL when validating group memberships.
+ ## @todo make base.py pass us the ksValidateFor_Xxxx value.
+ fIsAdding = bool(self.idTestBox in [ None, -1, '-1', 'None', '' ])
+
+ for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups):
+ oInSchedGroup = copy.copy(oInSchedGroup);
+ oInSchedGroup.idTestBox = self.idTestBox;
+ if fIsAdding:
+ dCurErrors = oInSchedGroup.validateAndConvertEx(['idTestBox',] + oInSchedGroup.kasAllowNullAttributes,
+ oDb, ModelDataBase.ksValidateFor_Add);
+ else:
+ dCurErrors = oInSchedGroup.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other);
+ if not dCurErrors:
+ pass; ## @todo figure out the ID?
+ else:
+ asErrors = [];
+ for sKey in dCurErrors:
+ asErrors.append('%s: %s' % (sKey[len('TestBoxInSchedGroup_'):],
+ dCurErrors[sKey] + ('{%s}' % self.idTestBox)))
+ dErrors[iInGrp] = '<br>\n'.join(asErrors)
+ aoNewValues.append(oInSchedGroup);
+
+ for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups):
+ for iInGrp2 in xrange(iInGrp + 1, len(self.aoInSchedGroups)):
+ if self.aoInSchedGroups[iInGrp2].idSchedGroup == oInSchedGroup.idSchedGroup:
+ sMsg = 'Duplicate scheduling group #%s".' % (oInSchedGroup.idSchedGroup,);
+ if iInGrp in dErrors: dErrors[iInGrp] += '<br>\n' + sMsg;
+ else: dErrors[iInGrp] = sMsg;
+ if iInGrp2 in dErrors: dErrors[iInGrp2] += '<br>\n' + sMsg;
+ else: dErrors[iInGrp2] = sMsg;
+ break;
+
+ return (aoNewValues, dErrors if dErrors else None);
+
+
+class TestBoxLogic(ModelLogicBase):
+ """
+ TestBox logic.
+ """
+
+ kiSortColumn_sName = 1;
+ kiSortColumn_sOs = 2;
+ kiSortColumn_sOsVersion = 3;
+ kiSortColumn_sCpuVendor = 4;
+ kiSortColumn_sCpuArch = 5;
+ kiSortColumn_lCpuRevision = 6;
+ kiSortColumn_cCpus = 7;
+ kiSortColumn_cMbMemory = 8;
+ kiSortColumn_cMbScratch = 9;
+ kiSortColumn_fCpuNestedPaging = 10;
+ kiSortColumn_iTestBoxScriptRev = 11;
+ kiSortColumn_iPythonHexVersion = 12;
+ kiSortColumn_enmPendingCmd = 13;
+ kiSortColumn_fEnabled = 14;
+ kiSortColumn_enmState = 15;
+ kiSortColumn_tsUpdated = 16;
+ kcMaxSortColumns = 17;
+ kdSortColumnMap = {
+ 0: 'TestBoxesWithStrings.sName',
+ kiSortColumn_sName: "regexp_replace(TestBoxesWithStrings.sName,'[0-9]*', '', 'g'), " \
+ "RIGHT(CONCAT(regexp_replace(TestBoxesWithStrings.sName,'[^0-9]*','', 'g'),'0'),8)::int",
+ -kiSortColumn_sName: "regexp_replace(TestBoxesWithStrings.sName,'[0-9]*', '', 'g') DESC, " \
+ "RIGHT(CONCAT(regexp_replace(TestBoxesWithStrings.sName,'[^0-9]*','', 'g'),'0'),8)::int DESC",
+ kiSortColumn_sOs: 'TestBoxesWithStrings.sOs',
+ -kiSortColumn_sOs: 'TestBoxesWithStrings.sOs DESC',
+ kiSortColumn_sOsVersion: 'TestBoxesWithStrings.sOsVersion',
+ -kiSortColumn_sOsVersion: 'TestBoxesWithStrings.sOsVersion DESC',
+ kiSortColumn_sCpuVendor: 'TestBoxesWithStrings.sCpuVendor',
+ -kiSortColumn_sCpuVendor: 'TestBoxesWithStrings.sCpuVendor DESC',
+ kiSortColumn_sCpuArch: 'TestBoxesWithStrings.sCpuArch',
+ -kiSortColumn_sCpuArch: 'TestBoxesWithStrings.sCpuArch DESC',
+ kiSortColumn_lCpuRevision: 'TestBoxesWithStrings.lCpuRevision',
+ -kiSortColumn_lCpuRevision: 'TestBoxesWithStrings.lCpuRevision DESC',
+ kiSortColumn_cCpus: 'TestBoxesWithStrings.cCpus',
+ -kiSortColumn_cCpus: 'TestBoxesWithStrings.cCpus DESC',
+ kiSortColumn_cMbMemory: 'TestBoxesWithStrings.cMbMemory',
+ -kiSortColumn_cMbMemory: 'TestBoxesWithStrings.cMbMemory DESC',
+ kiSortColumn_cMbScratch: 'TestBoxesWithStrings.cMbScratch',
+ -kiSortColumn_cMbScratch: 'TestBoxesWithStrings.cMbScratch DESC',
+ kiSortColumn_fCpuNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging',
+ -kiSortColumn_fCpuNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging DESC',
+ kiSortColumn_iTestBoxScriptRev: 'TestBoxesWithStrings.iTestBoxScriptRev',
+ -kiSortColumn_iTestBoxScriptRev: 'TestBoxesWithStrings.iTestBoxScriptRev DESC',
+ kiSortColumn_iPythonHexVersion: 'TestBoxesWithStrings.iPythonHexVersion',
+ -kiSortColumn_iPythonHexVersion: 'TestBoxesWithStrings.iPythonHexVersion DESC',
+ kiSortColumn_enmPendingCmd: 'TestBoxesWithStrings.enmPendingCmd',
+ -kiSortColumn_enmPendingCmd: 'TestBoxesWithStrings.enmPendingCmd DESC',
+ kiSortColumn_fEnabled: 'TestBoxesWithStrings.fEnabled',
+ -kiSortColumn_fEnabled: 'TestBoxesWithStrings.fEnabled DESC',
+ kiSortColumn_enmState: 'TestBoxStatuses.enmState',
+ -kiSortColumn_enmState: 'TestBoxStatuses.enmState DESC',
+ kiSortColumn_tsUpdated: 'TestBoxStatuses.tsUpdated',
+ -kiSortColumn_tsUpdated: 'TestBoxStatuses.tsUpdated DESC',
+ };
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb);
+ self.dCache = None;
+
+ def tryFetchTestBoxByUuid(self, sTestBoxUuid):
+ """
+ Tries to fetch a testbox by its UUID alone.
+ """
+ self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
+ 'FROM TestBoxesWithStrings\n'
+ 'WHERE uuidSystem = %s\n'
+ ' AND tsExpire = \'infinity\'::timestamp\n'
+ 'ORDER BY tsEffective DESC\n',
+ (sTestBoxUuid,));
+ if self._oDb.getRowCount() == 0:
+ return None;
+ if self._oDb.getRowCount() != 1:
+ raise TMTooManyRows('Database integrity error: %u hits' % (self._oDb.getRowCount(),));
+ oData = TestBoxData();
+ oData.initFromDbRow(self._oDb.fetchOne());
+ return oData;
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches testboxes for listing.
+
+ Returns an array (list) of TestBoxDataForListing items, empty list if none.
+ The TestBoxDataForListing instances are just TestBoxData with two extra
+ members, an extra oStatus member that is either None or a TestBoxStatusData
+ instance, and a member tsCurrent holding CURRENT_TIMESTAMP.
+
+ Raises exception on error.
+ """
+ class TestBoxDataForListing(TestBoxDataEx):
+ """ We add two members for the listing. """
+ def __init__(self):
+ TestBoxDataEx.__init__(self);
+ self.tsCurrent = None; # CURRENT_TIMESTAMP
+ self.oStatus = None # type: TestBoxStatusData
+
+ from testmanager.core.testboxstatus import TestBoxStatusData;
+
+ if not aiSortColumns:
+ aiSortColumns = [self.kiSortColumn_sName,];
+
+ if tsNow is None:
+ self._oDb.execute('SELECT TestBoxesWithStrings.*,\n'
+ ' TestBoxStatuses.*\n'
+ 'FROM TestBoxesWithStrings\n'
+ ' LEFT OUTER JOIN TestBoxStatuses\n'
+ ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n'
+ 'WHERE TestBoxesWithStrings.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY ' + (', '.join([self.kdSortColumnMap[i] for i in aiSortColumns])) + '\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT TestBoxesWithStrings.*,\n'
+ ' TestBoxStatuses.*\n'
+ 'FROM TestBoxesWithStrings\n'
+ ' LEFT OUTER JOIN TestBoxStatuses\n'
+ ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY ' + (', '.join([self.kdSortColumnMap[i] for i in aiSortColumns])) + '\n'
+ 'LIMIT %s OFFSET %s\n'
+ , ( tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRows = [];
+ for aoOne in self._oDb.fetchAll():
+ oTestBox = TestBoxDataForListing().initFromDbRowEx(aoOne, self._oDb, tsNow);
+ oTestBox.tsCurrent = self._oDb.getCurrentTimestamp();
+ if aoOne[TestBoxData.kcDbColumns] is not None:
+ oTestBox.oStatus = TestBoxStatusData().initFromDbRow(aoOne[TestBoxData.kcDbColumns:]);
+ aoRows.append(oTestBox);
+ return aoRows;
+
+ def fetchForSchedGroup(self, idSchedGroup, tsNow, aiSortColumns = None):
+ """
+ Fetches testboxes for listing.
+
+ Returns an array (list) of TestBoxDataForSchedGroup items, empty list if none.
+
+ Raises exception on error.
+ """
+ if not aiSortColumns:
+ aiSortColumns = [self.kiSortColumn_sName,];
+ asSortColumns = [self.kdSortColumnMap[i] for i in aiSortColumns];
+ asSortColumns.append('TestBoxesInSchedGroups.idTestBox');
+
+ if tsNow is None:
+ self._oDb.execute('''
+SELECT TestBoxesInSchedGroups.*,
+ TestBoxesWithStrings.*
+FROM TestBoxesInSchedGroups
+ LEFT OUTER JOIN TestBoxesWithStrings
+ ON TestBoxesWithStrings.idTestBox = TestBoxesInSchedGroups.idTestBox
+ AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
+WHERE TestBoxesInSchedGroups.idSchedGroup = %s
+ AND TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP
+ORDER BY ''' + ', '.join(asSortColumns), (idSchedGroup, ));
+ else:
+ self._oDb.execute('''
+SELECT TestBoxesInSchedGroups.*,
+ TestBoxesWithStrings.*
+FROM TestBoxesInSchedGroups
+ LEFT OUTER JOIN TestBoxesWithStrings
+ ON TestBoxesWithStrings.idTestBox = TestBoxesInSchedGroups.idTestBox
+ AND TestBoxesWithStrings.tsExpire > %s
+ AND TestBoxesWithStrings.tsEffective <= %s
+WHERE TestBoxesInSchedGroups.idSchedGroup = %s
+ AND TestBoxesInSchedGroups.tsExpire > %s
+ AND TestBoxesInSchedGroups.tsEffective <= %s
+ORDER BY ''' + ', '.join(asSortColumns), (tsNow, tsNow, idSchedGroup, tsNow, tsNow, ));
+
+ aoRows = [];
+ for aoOne in self._oDb.fetchAll():
+ aoRows.append(TestBoxDataForSchedGroup().initFromDbRow(aoOne));
+ return aoRows;
+
+ def fetchForChangeLog(self, idTestBox, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals
+ """
+ Fetches change log entries for a testbox.
+
+ Returns an array of ChangeLogEntry instance and an indicator whether
+ there are more entries.
+ Raises exception on error.
+ """
+
+ ## @todo calc changes to scheduler group!
+
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+
+ self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
+ 'FROM TestBoxesWithStrings\n'
+ 'WHERE TestBoxesWithStrings.tsEffective <= %s\n'
+ ' AND TestBoxesWithStrings.idTestBox = %s\n'
+ 'ORDER BY TestBoxesWithStrings.tsExpire DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, idTestBox, cMaxRows + 1, iStart,));
+
+ aoRows = [];
+ for aoDbRow in self._oDb.fetchAll():
+ aoRows.append(TestBoxData().initFromDbRow(aoDbRow));
+
+ # Calculate the changes.
+ aoEntries = [];
+ for i in xrange(0, len(aoRows) - 1):
+ oNew = aoRows[i];
+ oOld = aoRows[i + 1];
+ aoChanges = [];
+ for sAttr in oNew.getDataAttributes():
+ if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+ oOldAttr = getattr(oOld, sAttr);
+ oNewAttr = getattr(oNew, sAttr);
+ if oOldAttr != oNewAttr:
+ if sAttr == 'sReport':
+ aoChanges.append(AttributeChangeEntryPre(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+ else:
+ aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+ aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
+
+ # If we're at the end of the log, add the initial entry.
+ if len(aoRows) <= cMaxRows and aoRows:
+ oNew = aoRows[-1];
+ aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
+
+ UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries);
+ return (aoEntries, len(aoRows) > cMaxRows);
+
+ def _validateAndConvertData(self, oData, enmValidateFor):
+ # type: (TestBoxDataEx, str) -> None
+ """
+ Helper for addEntry and editEntry that validates the scheduling group IDs in
+ addtion to what's covered by the default validateAndConvert of the data object.
+
+ Raises exception on invalid input.
+ """
+ dDataErrors = oData.validateAndConvert(self._oDb, enmValidateFor);
+ if dDataErrors:
+ raise TMInvalidData('TestBoxLogic.addEntry: %s' % (dDataErrors,));
+ if isinstance(oData, TestBoxDataEx):
+ if oData.aoInSchedGroups:
+ sSchedGrps = ', '.join('(%s)' % oCur.idSchedGroup for oCur in oData.aoInSchedGroups);
+ self._oDb.execute('SELECT SchedGroupIDs.idSchedGroup\n'
+ 'FROM (VALUES ' + sSchedGrps + ' ) AS SchedGroupIDs(idSchedGroup)\n'
+ ' LEFT OUTER JOIN SchedGroups\n'
+ ' ON SchedGroupIDs.idSchedGroup = SchedGroups.idSchedGroup\n'
+ ' AND SchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'WHERE SchedGroups.idSchedGroup IS NULL\n');
+ aaoRows = self._oDb.fetchAll();
+ if aaoRows:
+ raise TMInvalidData('TestBoxLogic.addEntry missing scheduling groups: %s'
+ % (', '.join(str(aoRow[0]) for aoRow in aaoRows),));
+ return None;
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ # type: (TestBoxDataEx, int, bool) -> (int, int, datetime.datetime)
+ """
+ Creates a testbox in the database.
+ Returns the testbox ID, testbox generation ID and effective timestamp
+ of the created testbox on success. Throws error on failure.
+ """
+
+ #
+ # Validate. Extra work because of missing foreign key (due to history).
+ #
+ self._validateAndConvertData(oData, oData.ksValidateFor_Add);
+
+ #
+ # Do it.
+ #
+ self._oDb.callProc('TestBoxLogic_addEntry'
+ , ( uidAuthor,
+ oData.ip, # Should we allow setting the IP?
+ oData.uuidSystem,
+ oData.sName,
+ oData.sDescription,
+ oData.fEnabled,
+ oData.enmLomKind,
+ oData.ipLom,
+ oData.pctScaleTimeout,
+ oData.sComment,
+ oData.enmPendingCmd, ) );
+ (idTestBox, idGenTestBox, tsEffective) = self._oDb.fetchOne();
+
+ for oInSchedGrp in oData.aoInSchedGroups:
+ self._oDb.callProc('TestBoxLogic_addGroupEntry',
+ ( uidAuthor, idTestBox, oInSchedGrp.idSchedGroup, oInSchedGrp.iSchedPriority,) );
+
+ self._oDb.maybeCommit(fCommit);
+ return (idTestBox, idGenTestBox, tsEffective);
+
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Data edit update, web UI is the primary user.
+
+ oData is either TestBoxDataEx or TestBoxData. The latter is for enabling
+ Returns the new generation ID and effective date.
+ """
+
+ #
+ # Validate.
+ #
+ self._validateAndConvertData(oData, oData.ksValidateFor_Edit);
+
+ #
+ # Get current data.
+ #
+ oOldData = TestBoxDataEx().initFromDbWithId(self._oDb, oData.idTestBox);
+
+ #
+ # Do it.
+ #
+ if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', 'aoInSchedGroups', ]
+ + TestBoxData.kasMachineSettableOnly ):
+ self._oDb.callProc('TestBoxLogic_editEntry'
+ , ( uidAuthor,
+ oData.idTestBox,
+ oData.ip, # Should we allow setting the IP?
+ oData.uuidSystem,
+ oData.sName,
+ oData.sDescription,
+ oData.fEnabled,
+ oData.enmLomKind,
+ oData.ipLom,
+ oData.pctScaleTimeout,
+ oData.sComment,
+ oData.enmPendingCmd, ));
+ (idGenTestBox, tsEffective) = self._oDb.fetchOne();
+ else:
+ idGenTestBox = oOldData.idGenTestBox;
+ tsEffective = oOldData.tsEffective;
+
+ if isinstance(oData, TestBoxDataEx):
+ # Calc in-group changes.
+ aoRemoved = list(oOldData.aoInSchedGroups);
+ aoNew = [];
+ aoUpdated = [];
+ for oNewInGroup in oData.aoInSchedGroups:
+ oOldInGroup = None;
+ for iCur, oCur in enumerate(aoRemoved):
+ if oCur.idSchedGroup == oNewInGroup.idSchedGroup:
+ oOldInGroup = aoRemoved.pop(iCur);
+ break;
+ if oOldInGroup is None:
+ aoNew.append(oNewInGroup);
+ elif oNewInGroup.iSchedPriority != oOldInGroup.iSchedPriority:
+ aoUpdated.append(oNewInGroup);
+
+ # Remove in-groups.
+ for oInGroup in aoRemoved:
+ self._oDb.callProc('TestBoxLogic_removeGroupEntry', (uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, ));
+
+ # Add new ones.
+ for oInGroup in aoNew:
+ self._oDb.callProc('TestBoxLogic_addGroupEntry',
+ ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) );
+
+ # Edit existing ones.
+ for oInGroup in aoUpdated:
+ self._oDb.callProc('TestBoxLogic_editGroupEntry',
+ ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) );
+ else:
+ assert isinstance(oData, TestBoxData);
+
+ self._oDb.maybeCommit(fCommit);
+ return (idGenTestBox, tsEffective);
+
+
+ def removeEntry(self, uidAuthor, idTestBox, fCascade = False, fCommit = False):
+ """
+ Delete test box and scheduling group associations.
+ """
+ self._oDb.callProc('TestBoxLogic_removeEntry'
+ , ( uidAuthor, idTestBox, fCascade,));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def updateOnSignOn(self, idTestBox, idGenTestBox, sTestBoxAddr, sOs, sOsVersion, # pylint: disable=too-many-arguments,too-many-locals
+ sCpuVendor, sCpuArch, sCpuName, lCpuRevision, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest,
+ fChipsetIoMmu, fRawMode, cMbMemory, cMbScratch, sReport, iTestBoxScriptRev, iPythonHexVersion):
+ """
+ Update the testbox attributes automatically on behalf of the testbox script.
+ Returns the new generation id on success, raises an exception on failure.
+ """
+ _ = idGenTestBox;
+ self._oDb.callProc('TestBoxLogic_updateOnSignOn'
+ , ( idTestBox,
+ sTestBoxAddr,
+ sOs,
+ sOsVersion,
+ sCpuVendor,
+ sCpuArch,
+ sCpuName,
+ lCpuRevision,
+ cCpus,
+ fCpuHwVirt,
+ fCpuNestedPaging,
+ fCpu64BitGuest,
+ fChipsetIoMmu,
+ fRawMode,
+ cMbMemory,
+ cMbScratch,
+ sReport,
+ iTestBoxScriptRev,
+ iPythonHexVersion,));
+ return self._oDb.fetchOne()[0];
+
+
+ def setCommand(self, idTestBox, sOldCommand, sNewCommand, uidAuthor = None, fCommit = False, sComment = None):
+ """
+ Sets or resets the pending command on a testbox.
+ Returns (idGenTestBox, tsEffective) of the new row.
+ """
+ ## @todo throw TMInFligthCollision again...
+ self._oDb.callProc('TestBoxLogic_setCommand'
+ , ( uidAuthor, idTestBox, sOldCommand, sNewCommand, sComment,));
+ aoRow = self._oDb.fetchOne();
+ self._oDb.maybeCommit(fCommit);
+ return (aoRow[0], aoRow[1]);
+
+
+ def getAll(self):
+ """
+ Retrieve list of all registered Test Box records from DB.
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestBoxesWithStrings\n'
+ 'WHERE tsExpire=\'infinity\'::timestamp\n'
+ 'ORDER BY sName')
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(TestBoxData().initFromDbRow(aoRow))
+ return aoRet
+
+
+ def cachedLookup(self, idTestBox):
+ # type: (int) -> TestBoxDataEx
+ """
+ Looks up the most recent TestBoxData object for idTestBox via
+ an object cache.
+
+ Returns a shared TestBoxDataEx object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('TestBoxData');
+ oEntry = self.dCache.get(idTestBox, None);
+ if oEntry is None:
+ fNeedNow = False;
+ self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
+ 'FROM TestBoxesWithStrings\n'
+ 'WHERE idTestBox = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestBox, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
+ 'FROM TestBoxesWithStrings\n'
+ 'WHERE idTestBox = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idTestBox, ));
+ fNeedNow = True;
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestBox));
+
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ if not fNeedNow:
+ oEntry = TestBoxDataEx().initFromDbRowEx(aaoRow, self._oDb);
+ else:
+ oEntry = TestBoxDataEx().initFromDbRow(aaoRow);
+ oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow = db.dbTimestampMinusOneTick(oEntry.tsExpire));
+ self.dCache[idTestBox] = oEntry;
+ return oEntry;
+
+
+
+ #
+ # The virtual test sheriff interface.
+ #
+
+ def hasTestBoxRecentlyBeenRebooted(self, idTestBox, cHoursBack = 2, tsNow = None):
+ """
+ Checks if the testbox has been rebooted in the specified time period.
+
+ This does not include already pending reboots, though under some
+ circumstances it may. These being the test box entry being edited for
+ other reasons.
+
+ Returns True / False.
+ """
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('SELECT COUNT(idTestBox)\n'
+ 'FROM TestBoxes\n'
+ 'WHERE idTestBox = %s\n'
+ ' AND tsExpire < %s\n'
+ ' AND tsExpire >= %s - interval \'%s hours\'\n'
+ ' AND enmPendingCmd IN (%s, %s)\n'
+ , ( idTestBox, tsNow, tsNow, cHoursBack,
+ TestBoxData.ksTestBoxCmd_Reboot, TestBoxData.ksTestBoxCmd_UpgradeAndReboot, ));
+ return self._oDb.fetchOne()[0] > 0;
+
+
+ def rebootTestBox(self, idTestBox, uidAuthor, sComment, sOldCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False):
+ """
+ Issues a reboot command for the given test box.
+ Return True on succes, False on in-flight collision.
+ May raise DB exception on other trouble.
+ """
+ try:
+ self.setCommand(idTestBox, sOldCommand, TestBoxData.ksTestBoxCmd_Reboot,
+ uidAuthor = uidAuthor, fCommit = fCommit, sComment = sComment);
+ except TMInFligthCollision:
+ return False;
+ return True;
+
+
+ def disableTestBox(self, idTestBox, uidAuthor, sComment, fCommit = False):
+ """
+ Disables the given test box.
+
+ Raises exception on trouble, without rollback.
+ """
+ oTestBox = TestBoxData().initFromDbWithId(self._oDb, idTestBox);
+ if oTestBox.fEnabled:
+ oTestBox.fEnabled = False;
+ if sComment is not None:
+ oTestBox.sComment = sComment;
+ self.editEntry(oTestBox, uidAuthor = uidAuthor, fCommit = fCommit);
+ return None;
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class TestBoxDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestBoxData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testboxcontroller.py b/src/VBox/ValidationKit/testmanager/core/testboxcontroller.py
new file mode 100755
index 00000000..b131aa88
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testboxcontroller.py
@@ -0,0 +1,954 @@
+# -*- coding: utf-8 -*-
+# $Id: testboxcontroller.py $
+
+"""
+Test Manager Core - Web Server Abstraction Base Class.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import re;
+import os;
+import string; # pylint: disable=deprecated-module
+import sys;
+import uuid;
+
+# Validation Kit imports.
+from common import constants;
+from testmanager import config;
+from testmanager.core import coreconsts;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.base import TMExceptionBase;
+from testmanager.core.globalresource import GlobalResourceLogic;
+from testmanager.core.testboxstatus import TestBoxStatusData, TestBoxStatusLogic;
+from testmanager.core.testbox import TestBoxData, TestBoxLogic;
+from testmanager.core.testresults import TestResultLogic, TestResultFileData;
+from testmanager.core.testset import TestSetData, TestSetLogic;
+from testmanager.core.systemlog import SystemLogData, SystemLogLogic;
+from testmanager.core.schedulerbase import SchedulerBase;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ long = int; # pylint: disable=redefined-builtin,invalid-name
+
+
+class TestBoxControllerException(TMExceptionBase):
+ """
+ Exception class for TestBoxController.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TestBoxController(object): # pylint: disable=too-few-public-methods
+ """
+ TestBox Controller class.
+ """
+
+ ## Applicable testbox commands to an idle TestBox.
+ kasIdleCmds = [TestBoxData.ksTestBoxCmd_Reboot,
+ TestBoxData.ksTestBoxCmd_Upgrade,
+ TestBoxData.ksTestBoxCmd_UpgradeAndReboot,
+ TestBoxData.ksTestBoxCmd_Special];
+ ## Applicable testbox commands to a busy TestBox.
+ kasBusyCmds = [TestBoxData.ksTestBoxCmd_Abort, TestBoxData.ksTestBoxCmd_Reboot];
+ ## Commands that can be ACK'ed.
+ kasAckableCmds = [constants.tbresp.CMD_EXEC, constants.tbresp.CMD_ABORT, constants.tbresp.CMD_REBOOT,
+ constants.tbresp.CMD_UPGRADE, constants.tbresp.CMD_UPGRADE_AND_REBOOT, constants.tbresp.CMD_SPECIAL];
+ ## Commands that can be NACK'ed or NOTSUP'ed.
+ kasNackableCmds = kasAckableCmds + [kasAckableCmds, constants.tbresp.CMD_IDLE, constants.tbresp.CMD_WAIT];
+
+ ## Mapping from TestBoxCmd_T to TestBoxState_T
+ kdCmdToState = \
+ { \
+ TestBoxData.ksTestBoxCmd_Abort: None,
+ TestBoxData.ksTestBoxCmd_Reboot: TestBoxStatusData.ksTestBoxState_Rebooting,
+ TestBoxData.ksTestBoxCmd_Upgrade: TestBoxStatusData.ksTestBoxState_Upgrading,
+ TestBoxData.ksTestBoxCmd_UpgradeAndReboot: TestBoxStatusData.ksTestBoxState_UpgradingAndRebooting,
+ TestBoxData.ksTestBoxCmd_Special: TestBoxStatusData.ksTestBoxState_DoingSpecialCmd,
+ };
+
+ ## Mapping from TestBoxCmd_T to TestBox responses commands.
+ kdCmdToTbRespCmd = \
+ {
+ TestBoxData.ksTestBoxCmd_Abort: constants.tbresp.CMD_ABORT,
+ TestBoxData.ksTestBoxCmd_Reboot: constants.tbresp.CMD_REBOOT,
+ TestBoxData.ksTestBoxCmd_Upgrade: constants.tbresp.CMD_UPGRADE,
+ TestBoxData.ksTestBoxCmd_UpgradeAndReboot: constants.tbresp.CMD_UPGRADE_AND_REBOOT,
+ TestBoxData.ksTestBoxCmd_Special: constants.tbresp.CMD_SPECIAL,
+ };
+
+ ## Mapping from TestBox responses to TestBoxCmd_T commands.
+ kdTbRespCmdToCmd = \
+ {
+ constants.tbresp.CMD_IDLE: None,
+ constants.tbresp.CMD_WAIT: None,
+ constants.tbresp.CMD_EXEC: None,
+ constants.tbresp.CMD_ABORT: TestBoxData.ksTestBoxCmd_Abort,
+ constants.tbresp.CMD_REBOOT: TestBoxData.ksTestBoxCmd_Reboot,
+ constants.tbresp.CMD_UPGRADE: TestBoxData.ksTestBoxCmd_Upgrade,
+ constants.tbresp.CMD_UPGRADE_AND_REBOOT: TestBoxData.ksTestBoxCmd_UpgradeAndReboot,
+ constants.tbresp.CMD_SPECIAL: TestBoxData.ksTestBoxCmd_Special,
+ };
+
+
+ ## The path to the upgrade zip, relative WebServerGlueBase.getBaseUrl().
+ ksUpgradeZip = 'htdocs/upgrade/VBoxTestBoxScript.zip';
+
+ ## Valid TestBox result values.
+ kasValidResults = list(constants.result.g_kasValidResults);
+ ## Mapping TestBox result values to TestStatus_T values.
+ kadTbResultToStatus = \
+ {
+ constants.result.PASSED: TestSetData.ksTestStatus_Success,
+ constants.result.SKIPPED: TestSetData.ksTestStatus_Skipped,
+ constants.result.ABORTED: TestSetData.ksTestStatus_Aborted,
+ constants.result.BAD_TESTBOX: TestSetData.ksTestStatus_BadTestBox,
+ constants.result.FAILED: TestSetData.ksTestStatus_Failure,
+ constants.result.TIMED_OUT: TestSetData.ksTestStatus_TimedOut,
+ constants.result.REBOOTED: TestSetData.ksTestStatus_Rebooted,
+ };
+
+
+ def __init__(self, oSrvGlue):
+ """
+ Won't raise exceptions.
+ """
+ self._oSrvGlue = oSrvGlue;
+ self._sAction = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._idTestBox = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._sTestBoxUuid = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._sTestBoxAddr = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._idTestSet = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._dParams = None; # _getStandardParams / dispatchRequest sets this later on.
+ self._asCheckedParams = [];
+ self._dActions = \
+ { \
+ constants.tbreq.SIGNON : self._actionSignOn,
+ constants.tbreq.REQUEST_COMMAND_BUSY: self._actionRequestCommandBusy,
+ constants.tbreq.REQUEST_COMMAND_IDLE: self._actionRequestCommandIdle,
+ constants.tbreq.COMMAND_ACK : self._actionCommandAck,
+ constants.tbreq.COMMAND_NACK : self._actionCommandNack,
+ constants.tbreq.COMMAND_NOTSUP : self._actionCommandNotSup,
+ constants.tbreq.LOG_MAIN : self._actionLogMain,
+ constants.tbreq.UPLOAD : self._actionUpload,
+ constants.tbreq.XML_RESULTS : self._actionXmlResults,
+ constants.tbreq.EXEC_COMPLETED : self._actionExecCompleted,
+ };
+
+ def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None):
+ """
+ Gets a string parameter (stripped).
+
+ Raises exception if not found and no default is provided, or if the
+ value isn't found in asValidValues.
+ """
+ if sName not in self._dParams:
+ if sDefValue is None:
+ raise TestBoxControllerException('%s parameter %s is missing' % (self._sAction, sName));
+ return sDefValue;
+ sValue = self._dParams[sName];
+ if fStrip:
+ sValue = sValue.strip();
+
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ if asValidValues is not None and sValue not in asValidValues:
+ raise TestBoxControllerException('%s parameter %s value "%s" not in %s ' \
+ % (self._sAction, sName, sValue, asValidValues));
+ return sValue;
+
+ def _getBoolParam(self, sName, fDefValue = None):
+ """
+ Gets a boolean parameter.
+
+ Raises exception if not found and no default is provided, or if not a
+ valid boolean.
+ """
+ sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue));
+ return sValue in ('True', 'true', '1',);
+
+ def _getIntParam(self, sName, iMin = None, iMax = None):
+ """
+ Gets a string parameter.
+ Raises exception if not found, not a valid integer, or if the value
+ isn't in the range defined by iMin and iMax.
+ """
+ sValue = self._getStringParam(sName);
+ try:
+ iValue = int(sValue, 0);
+ except:
+ raise TestBoxControllerException('%s parameter %s value "%s" cannot be convert to an integer' \
+ % (self._sAction, sName, sValue));
+
+ if (iMin is not None and iValue < iMin) \
+ or (iMax is not None and iValue > iMax):
+ raise TestBoxControllerException('%s parameter %s value %d is out of range [%s..%s]' \
+ % (self._sAction, sName, iValue, iMin, iMax));
+ return iValue;
+
+ def _getLongParam(self, sName, lMin = None, lMax = None, lDefValue = None):
+ """
+ Gets a string parameter.
+ Raises exception if not found, not a valid long integer, or if the value
+ isn't in the range defined by lMin and lMax.
+ """
+ sValue = self._getStringParam(sName, sDefValue = (str(lDefValue) if lDefValue is not None else None));
+ try:
+ lValue = long(sValue, 0);
+ except Exception as oXcpt:
+ raise TestBoxControllerException('%s parameter %s value "%s" cannot be convert to an integer (%s)' \
+ % (self._sAction, sName, sValue, oXcpt));
+
+ if (lMin is not None and lValue < lMin) \
+ or (lMax is not None and lValue > lMax):
+ raise TestBoxControllerException('%s parameter %s value %d is out of range [%s..%s]' \
+ % (self._sAction, sName, lValue, lMin, lMax));
+ return lValue;
+
+ def _checkForUnknownParameters(self):
+ """
+ Check if we've handled all parameters, raises exception if anything
+ unknown was found.
+ """
+
+ if len(self._asCheckedParams) != len(self._dParams):
+ sUnknownParams = '';
+ for sKey in self._dParams:
+ if sKey not in self._asCheckedParams:
+ sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey];
+ raise TestBoxControllerException('Unknown parameters: ' + sUnknownParams);
+
+ return True;
+
+ def _writeResponse(self, dParams):
+ """
+ Makes a reply to the testbox script.
+ Will raise exception on failure.
+ """
+ self._oSrvGlue.writeParams(dParams);
+ self._oSrvGlue.flush();
+ return True;
+
+ def _resultResponse(self, sResultValue):
+ """
+ Makes a simple reply to the testbox script.
+ Will raise exception on failure.
+ """
+ return self._writeResponse({constants.tbresp.ALL_PARAM_RESULT: sResultValue});
+
+
+ def _idleResponse(self):
+ """
+ Makes an IDLE reply to the testbox script.
+ Will raise exception on failure.
+ """
+ return self._writeResponse({ constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_IDLE });
+
+ def _cleanupOldTest(self, oDb, oStatusData):
+ """
+ Cleans up any old test set that may be left behind and changes the
+ state to 'idle'. See scenario #9:
+ file://../../docs/AutomaticTestingRevamp.html#cleaning-up-abandoned-testcase
+
+ Note. oStatusData.enmState is set to idle, but tsUpdated is not changed.
+ """
+
+ # Cleanup any abandoned test.
+ if oStatusData.idTestSet is not None:
+ SystemLogLogic(oDb).addEntry(SystemLogData.ksEvent_TestSetAbandoned,
+ "idTestSet=%u idTestBox=%u enmState=%s %s"
+ % (oStatusData.idTestSet, oStatusData.idTestBox,
+ oStatusData.enmState, self._sAction),
+ fCommit = False);
+ TestSetLogic(oDb).completeAsAbandoned(oStatusData.idTestSet, fCommit = False);
+ GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False);
+
+ # Change to idle status
+ if oStatusData.enmState != TestBoxStatusData.ksTestBoxState_Idle:
+ TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
+ oStatusData.tsUpdated = oDb.getCurrentTimestamp();
+ oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle;
+
+ # Commit.
+ oDb.commit();
+
+ return True;
+
+ def _connectToDbAndValidateTb(self, asValidStates = None):
+ """
+ Connects to the database and validates the testbox.
+
+ Returns (TMDatabaseConnection, TestBoxStatusData, TestBoxData) on success.
+ Returns (None, None, None) on failure after sending the box an appropriate response.
+ May raise exception on DB error.
+ """
+ oDb = TMDatabaseConnection(self._oSrvGlue.dprint);
+ oLogic = TestBoxStatusLogic(oDb);
+ (oStatusData, oTestBoxData) = oLogic.tryFetchStatusAndConfig(self._idTestBox, self._sTestBoxUuid, self._sTestBoxAddr);
+ if oStatusData is None:
+ self._resultResponse(constants.tbresp.STATUS_DEAD);
+ elif asValidStates is not None and oStatusData.enmState not in asValidStates:
+ self._resultResponse(constants.tbresp.STATUS_NACK);
+ elif self._idTestSet is not None and self._idTestSet != oStatusData.idTestSet:
+ self._resultResponse(constants.tbresp.STATUS_NACK);
+ else:
+ return (oDb, oStatusData, oTestBoxData);
+ return (None, None, None);
+
+ def writeToMainLog(self, oTestSet, sText, fIgnoreSizeCheck = False):
+ """ Writes the text to the main log file. """
+
+ # Calc the file name and open the file.
+ sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-main.log');
+ if not os.path.exists(os.path.dirname(sFile)):
+ os.makedirs(os.path.dirname(sFile), 0o755);
+
+ with open(sFile, 'ab') as oFile:
+ # Check the size.
+ fSizeOk = True;
+ if not fIgnoreSizeCheck:
+ oStat = os.fstat(oFile.fileno());
+ fSizeOk = oStat.st_size / (1024 * 1024) < config.g_kcMbMaxMainLog;
+
+ # Write the text.
+ if fSizeOk:
+ if sys.version_info[0] >= 3:
+ oFile.write(bytes(sText, 'utf-8'));
+ else:
+ oFile.write(sText);
+
+ return fSizeOk;
+
+ def _actionSignOn(self): # pylint: disable=too-many-locals
+ """ Implement sign-on """
+
+ #
+ # Validate parameters (raises exception on failure).
+ #
+ sOs = self._getStringParam(constants.tbreq.SIGNON_PARAM_OS, coreconsts.g_kasOses);
+ sOsVersion = self._getStringParam(constants.tbreq.SIGNON_PARAM_OS_VERSION);
+ sCpuVendor = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_VENDOR);
+ sCpuArch = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_ARCH, coreconsts.g_kasCpuArches);
+ sCpuName = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_NAME, fStrip = True, sDefValue = ''); # new
+ lCpuRevision = self._getLongParam( constants.tbreq.SIGNON_PARAM_CPU_REVISION, lMin = 0, lDefValue = 0); # new
+ cCpus = self._getIntParam( constants.tbreq.SIGNON_PARAM_CPU_COUNT, 1, 16384);
+ fCpuHwVirt = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_HW_VIRT);
+ fCpuNestedPaging = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_NESTED_PAGING);
+ fCpu64BitGuest = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_64_BIT_GUEST, fDefValue = True);
+ fChipsetIoMmu = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_IOMMU);
+ fRawMode = self._getBoolParam( constants.tbreq.SIGNON_PARAM_WITH_RAW_MODE, fDefValue = None);
+ cMbMemory = self._getLongParam( constants.tbreq.SIGNON_PARAM_MEM_SIZE, 8, 1073741823); # 8MB..1PB
+ cMbScratch = self._getLongParam( constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE, 0, 1073741823); # 0..1PB
+ sReport = self._getStringParam(constants.tbreq.SIGNON_PARAM_REPORT, fStrip = True, sDefValue = ''); # new
+ iTestBoxScriptRev = self._getIntParam( constants.tbreq.SIGNON_PARAM_SCRIPT_REV, 1, 100000000);
+ iPythonHexVersion = self._getIntParam( constants.tbreq.SIGNON_PARAM_PYTHON_VERSION, 0x020300f0, 0x030f00f0);
+ self._checkForUnknownParameters();
+
+ # Null conversions for new parameters.
+ if not sReport:
+ sReport = None;
+ if not sCpuName:
+ sCpuName = None;
+ if lCpuRevision <= 0:
+ lCpuRevision = None;
+
+ #
+ # Connect to the database and validate the testbox.
+ #
+ oDb = TMDatabaseConnection(self._oSrvGlue.dprint);
+ oTestBoxLogic = TestBoxLogic(oDb);
+ oTestBox = oTestBoxLogic.tryFetchTestBoxByUuid(self._sTestBoxUuid);
+ if oTestBox is None:
+ oSystemLogLogic = SystemLogLogic(oDb);
+ oSystemLogLogic.addEntry(SystemLogData.ksEvent_TestBoxUnknown,
+ 'addr=%s uuid=%s os=%s %d cpus' \
+ % (self._sTestBoxAddr, self._sTestBoxUuid, sOs, cCpus),
+ 24, fCommit = True);
+ return self._resultResponse(constants.tbresp.STATUS_NACK);
+
+ #
+ # Update the row in TestBoxes if something changed.
+ #
+ if oTestBox.cMbScratch is not None and oTestBox.cMbScratch != 0:
+ cPctScratchDiff = (cMbScratch - oTestBox.cMbScratch) * 100 / oTestBox.cMbScratch;
+ else:
+ cPctScratchDiff = 100;
+
+ # pylint: disable=too-many-boolean-expressions
+ if self._sTestBoxAddr != oTestBox.ip \
+ or sOs != oTestBox.sOs \
+ or sOsVersion != oTestBox.sOsVersion \
+ or sCpuVendor != oTestBox.sCpuVendor \
+ or sCpuArch != oTestBox.sCpuArch \
+ or sCpuName != oTestBox.sCpuName \
+ or lCpuRevision != oTestBox.lCpuRevision \
+ or cCpus != oTestBox.cCpus \
+ or fCpuHwVirt != oTestBox.fCpuHwVirt \
+ or fCpuNestedPaging != oTestBox.fCpuNestedPaging \
+ or fCpu64BitGuest != oTestBox.fCpu64BitGuest \
+ or fChipsetIoMmu != oTestBox.fChipsetIoMmu \
+ or fRawMode != oTestBox.fRawMode \
+ or cMbMemory != oTestBox.cMbMemory \
+ or abs(cPctScratchDiff) >= min(4 + cMbScratch / 10240, 12) \
+ or sReport != oTestBox.sReport \
+ or iTestBoxScriptRev != oTestBox.iTestBoxScriptRev \
+ or iPythonHexVersion != oTestBox.iPythonHexVersion:
+ oTestBoxLogic.updateOnSignOn(oTestBox.idTestBox,
+ oTestBox.idGenTestBox,
+ sTestBoxAddr = self._sTestBoxAddr,
+ sOs = sOs,
+ sOsVersion = sOsVersion,
+ sCpuVendor = sCpuVendor,
+ sCpuArch = sCpuArch,
+ sCpuName = sCpuName,
+ lCpuRevision = lCpuRevision,
+ cCpus = cCpus,
+ fCpuHwVirt = fCpuHwVirt,
+ fCpuNestedPaging = fCpuNestedPaging,
+ fCpu64BitGuest = fCpu64BitGuest,
+ fChipsetIoMmu = fChipsetIoMmu,
+ fRawMode = fRawMode,
+ cMbMemory = cMbMemory,
+ cMbScratch = cMbScratch,
+ sReport = sReport,
+ iTestBoxScriptRev = iTestBoxScriptRev,
+ iPythonHexVersion = iPythonHexVersion);
+
+ #
+ # Update the testbox status, making sure there is a status.
+ #
+ oStatusLogic = TestBoxStatusLogic(oDb);
+ oStatusData = oStatusLogic.tryFetchStatus(oTestBox.idTestBox);
+ if oStatusData is not None:
+ self._cleanupOldTest(oDb, oStatusData);
+ else:
+ oStatusLogic.insertIdleStatus(oTestBox.idTestBox, oTestBox.idGenTestBox, fCommit = True);
+
+ #
+ # ACK the request.
+ #
+ dResponse = \
+ {
+ constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.STATUS_ACK,
+ constants.tbresp.SIGNON_PARAM_ID: oTestBox.idTestBox,
+ constants.tbresp.SIGNON_PARAM_NAME: oTestBox.sName,
+ }
+ return self._writeResponse(dResponse);
+
+ def _doGangCleanup(self, oDb, oStatusData):
+ """
+ _doRequestCommand worker for handling a box in gang-cleanup.
+ This will check if all testboxes has completed their run, pretending to
+ be busy until that happens. Once all are completed, resources will be
+ freed and the testbox returns to idle state (we update oStatusData).
+ """
+ oStatusLogic = TestBoxStatusLogic(oDb)
+ oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
+ if oStatusLogic.isWholeGangDoneTesting(oTestSet.idTestSetGangLeader):
+ oDb.begin();
+
+ GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False);
+ TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
+
+ oStatusData.tsUpdated = oDb.getCurrentTimestamp();
+ oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle;
+
+ oDb.commit();
+ return None;
+
+ def _doGangGatheringTimedOut(self, oDb, oStatusData):
+ """
+ _doRequestCommand worker for handling a box in gang-gathering-timed-out state.
+ This will do clean-ups similar to _cleanupOldTest and update the state likewise.
+ """
+ oDb.begin();
+
+ TestSetLogic(oDb).completeAsGangGatheringTimeout(oStatusData.idTestSet, fCommit = False);
+ GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False);
+ TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
+
+ oStatusData.tsUpdated = oDb.getCurrentTimestamp();
+ oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle;
+
+ oDb.commit();
+ return None;
+
+ def _doGangGathering(self, oDb, oStatusData):
+ """
+ _doRequestCommand worker for handling a box in gang-gathering state.
+ This only checks for timeout. It will update the oStatusData if a
+ timeout is detected, so that the box will be idle upon return.
+ """
+ oStatusLogic = TestBoxStatusLogic(oDb);
+ if oStatusLogic.timeSinceLastChangeInSecs(oStatusData) > config.g_kcSecGangGathering \
+ and SchedulerBase.tryCancelGangGathering(oDb, oStatusData): # <-- Updates oStatusData.
+ self._doGangGatheringTimedOut(oDb, oStatusData);
+ return None;
+
+ def _doRequestCommand(self, fIdle):
+ """
+ Common code for handling command request.
+ """
+
+ (oDb, oStatusData, oTestBoxData) = self._connectToDbAndValidateTb();
+ if oDb is None:
+ return False;
+
+ #
+ # Status clean up.
+ #
+ # Only when BUSY will the TestBox Script request and execute commands
+ # concurrently. So, it must be idle when sending REQUEST_COMMAND_IDLE.
+ #
+ if fIdle:
+ if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGathering:
+ self._doGangGathering(oDb, oStatusData);
+ elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut:
+ self._doGangGatheringTimedOut(oDb, oStatusData);
+ elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangTesting:
+ dResponse = SchedulerBase.composeExecResponse(oDb, oTestBoxData.idTestBox, self._oSrvGlue.getBaseUrl());
+ if dResponse is not None:
+ return dResponse;
+ elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangCleanup:
+ self._doGangCleanup(oDb, oStatusData);
+ elif oStatusData.enmState != TestBoxStatusData.ksTestBoxState_Idle: # (includes ksTestBoxState_GangGatheringTimedOut)
+ self._cleanupOldTest(oDb, oStatusData);
+
+ #
+ # Check for pending command.
+ #
+ if oTestBoxData.enmPendingCmd != TestBoxData.ksTestBoxCmd_None:
+ asValidCmds = TestBoxController.kasIdleCmds if fIdle else TestBoxController.kasBusyCmds;
+ if oTestBoxData.enmPendingCmd in asValidCmds:
+ dResponse = { constants.tbresp.ALL_PARAM_RESULT: TestBoxController.kdCmdToTbRespCmd[oTestBoxData.enmPendingCmd] };
+ if oTestBoxData.enmPendingCmd in [TestBoxData.ksTestBoxCmd_Upgrade, TestBoxData.ksTestBoxCmd_UpgradeAndReboot]:
+ dResponse[constants.tbresp.UPGRADE_PARAM_URL] = self._oSrvGlue.getBaseUrl() + TestBoxController.ksUpgradeZip;
+ return self._writeResponse(dResponse);
+
+ if oTestBoxData.enmPendingCmd == TestBoxData.ksTestBoxCmd_Abort and fIdle:
+ TestBoxLogic(oDb).setCommand(self._idTestBox, sOldCommand = oTestBoxData.enmPendingCmd,
+ sNewCommand = TestBoxData.ksTestBoxCmd_None, fCommit = True);
+
+ #
+ # If doing gang stuff, return 'CMD_WAIT'.
+ #
+ ## @todo r=bird: Why is GangTesting included here? Figure out when testing gang testing.
+ if oStatusData.enmState in [TestBoxStatusData.ksTestBoxState_GangGathering,
+ TestBoxStatusData.ksTestBoxState_GangTesting,
+ TestBoxStatusData.ksTestBoxState_GangCleanup]:
+ return self._resultResponse(constants.tbresp.CMD_WAIT);
+
+ #
+ # If idling and enabled try schedule a new task.
+ #
+ if fIdle \
+ and oTestBoxData.fEnabled \
+ and not TestSetLogic(oDb).isTestBoxExecutingTooRapidly(oTestBoxData.idTestBox) \
+ and oStatusData.enmState == TestBoxStatusData.ksTestBoxState_Idle: # (paranoia)
+ dResponse = SchedulerBase.scheduleNewTask(oDb, oTestBoxData, oStatusData.iWorkItem, self._oSrvGlue.getBaseUrl());
+ if dResponse is not None:
+ return self._writeResponse(dResponse);
+
+ #
+ # Touch the status row every couple of mins so we can tell that the box is alive.
+ #
+ oStatusLogic = TestBoxStatusLogic(oDb);
+ if oStatusData.enmState != TestBoxStatusData.ksTestBoxState_GangGathering \
+ and oStatusLogic.timeSinceLastChangeInSecs(oStatusData) >= TestBoxStatusLogic.kcSecIdleTouchStatus:
+ oStatusLogic.touchStatus(oTestBoxData.idTestBox, fCommit = True);
+
+ return self._idleResponse();
+
+ def _actionRequestCommandBusy(self):
+ """ Implement request for command. """
+ self._checkForUnknownParameters();
+ return self._doRequestCommand(False);
+
+ def _actionRequestCommandIdle(self):
+ """ Implement request for command. """
+ self._checkForUnknownParameters();
+ return self._doRequestCommand(True);
+
+ def _doCommandAckNck(self, sCmd):
+ """ Implements ACK, NACK and NACK(ENOTSUP). """
+
+ (oDb, _, _) = self._connectToDbAndValidateTb();
+ if oDb is None:
+ return False;
+
+ #
+ # If the command maps to a TestBoxCmd_T value, it means we have to
+ # check and update TestBoxes. If it's an ACK, the testbox status will
+ # need updating as well.
+ #
+ sPendingCmd = TestBoxController.kdTbRespCmdToCmd[sCmd];
+ if sPendingCmd is not None:
+ oTestBoxLogic = TestBoxLogic(oDb)
+ oTestBoxLogic.setCommand(self._idTestBox, sOldCommand = sPendingCmd,
+ sNewCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False);
+
+ if self._sAction == constants.tbreq.COMMAND_ACK \
+ and TestBoxController.kdCmdToState[sPendingCmd] is not None:
+ oStatusLogic = TestBoxStatusLogic(oDb);
+ oStatusLogic.updateState(self._idTestBox, TestBoxController.kdCmdToState[sPendingCmd], fCommit = False);
+
+ # Commit the two updates.
+ oDb.commit();
+
+ #
+ # Log NACKs.
+ #
+ if self._sAction != constants.tbreq.COMMAND_ACK:
+ oSysLogLogic = SystemLogLogic(oDb);
+ oSysLogLogic.addEntry(SystemLogData.ksEvent_CmdNacked,
+ 'idTestBox=%s sCmd=%s' % (self._idTestBox, sPendingCmd),
+ 24, fCommit = True);
+
+ return self._resultResponse(constants.tbresp.STATUS_ACK);
+
+ def _actionCommandAck(self):
+ """ Implement command ACK'ing """
+ sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasAckableCmds);
+ self._checkForUnknownParameters();
+ return self._doCommandAckNck(sCmd);
+
+ def _actionCommandNack(self):
+ """ Implement command NACK'ing """
+ sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasNackableCmds);
+ self._checkForUnknownParameters();
+ return self._doCommandAckNck(sCmd);
+
+ def _actionCommandNotSup(self):
+ """ Implement command NACK(ENOTSUP)'ing """
+ sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasNackableCmds);
+ self._checkForUnknownParameters();
+ return self._doCommandAckNck(sCmd);
+
+ def _actionLogMain(self):
+ """ Implement submitting log entries to the main log file. """
+ #
+ # Parameter validation.
+ #
+ sBody = self._getStringParam(constants.tbreq.LOG_PARAM_BODY, fStrip = False);
+ if not sBody:
+ return self._resultResponse(constants.tbresp.STATUS_NACK);
+ self._checkForUnknownParameters();
+
+ (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
+ TestBoxStatusData.ksTestBoxState_GangTesting]);
+ if oStatusData is None:
+ return False;
+
+ #
+ # Write the text to the log file.
+ #
+ oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
+ self.writeToMainLog(oTestSet, sBody);
+ ## @todo Overflow is a hanging offence, need to note it and fail whatever is going on...
+
+ # Done.
+ return self._resultResponse(constants.tbresp.STATUS_ACK);
+
+ def _actionUpload(self):
+ """ Implement uploading of files. """
+ #
+ # Parameter validation.
+ #
+ sName = self._getStringParam(constants.tbreq.UPLOAD_PARAM_NAME);
+ sMime = self._getStringParam(constants.tbreq.UPLOAD_PARAM_MIME);
+ sKind = self._getStringParam(constants.tbreq.UPLOAD_PARAM_KIND);
+ sDesc = self._getStringParam(constants.tbreq.UPLOAD_PARAM_DESC);
+ self._checkForUnknownParameters();
+
+ (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
+ TestBoxStatusData.ksTestBoxState_GangTesting]);
+ if oStatusData is None:
+ return False;
+
+ if len(sName) > 128 or len(sName) < 3:
+ raise TestBoxControllerException('Invalid file name "%s"' % (sName,));
+ if re.match(r'^[a-zA-Z0-9_\-(){}#@+,.=]*$', sName) is None:
+ raise TestBoxControllerException('Invalid file name "%s"' % (sName,));
+
+ if sMime not in [ 'text/plain', #'text/html', 'text/xml',
+ 'application/octet-stream',
+ 'image/png', #'image/gif', 'image/jpeg',
+ 'video/webm', #'video/mpeg', 'video/mpeg4-generic',
+ ]:
+ raise TestBoxControllerException('Invalid MIME type "%s"' % (sMime,));
+
+ if sKind not in TestResultFileData.kasKinds:
+ raise TestBoxControllerException('Invalid kind "%s"' % (sKind,));
+
+ if len(sDesc) > 256:
+ raise TestBoxControllerException('Invalid description "%s"' % (sDesc,));
+ if not set(sDesc).issubset(set(string.printable)):
+ raise TestBoxControllerException('Invalid description "%s"' % (sDesc,));
+
+ if ('application/octet-stream', {}) != self._oSrvGlue.getContentType():
+ raise TestBoxControllerException('Unexpected content type: %s; %s' % self._oSrvGlue.getContentType());
+
+ cbFile = self._oSrvGlue.getContentLength();
+ if cbFile <= 0:
+ raise TestBoxControllerException('File "%s" is empty or negative in size (%s)' % (sName, cbFile));
+ if (cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadSingle:
+ raise TestBoxControllerException('File "%s" is too big %u bytes (max %u MiB)'
+ % (sName, cbFile, config.g_kcMbMaxUploadSingle,));
+
+ #
+ # Write the text to the log file.
+ #
+ oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
+ oDstFile = TestSetLogic(oDb).createFile(oTestSet, sName = sName, sMime = sMime, sKind = sKind, sDesc = sDesc,
+ cbFile = cbFile, fCommit = True);
+
+ offFile = 0;
+ oSrcFile = self._oSrvGlue.getBodyIoStreamBinary();
+ while offFile < cbFile:
+ cbToRead = cbFile - offFile;
+ if cbToRead > 256*1024:
+ cbToRead = 256*1024;
+ offFile += cbToRead;
+
+ abBuf = oSrcFile.read(cbToRead);
+ oDstFile.write(abBuf); # pylint: disable=maybe-no-member
+ del abBuf;
+
+ oDstFile.close(); # pylint: disable=maybe-no-member
+
+ # Done.
+ return self._resultResponse(constants.tbresp.STATUS_ACK);
+
+ def _actionXmlResults(self):
+ """ Implement submitting "XML" like test result stream. """
+ #
+ # Parameter validation.
+ #
+ sXml = self._getStringParam(constants.tbreq.XML_RESULT_PARAM_BODY, fStrip = False);
+ self._checkForUnknownParameters();
+ if not sXml: # Used for link check by vboxinstaller.py on Windows.
+ return self._resultResponse(constants.tbresp.STATUS_ACK);
+
+ (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
+ TestBoxStatusData.ksTestBoxState_GangTesting]);
+ if oStatusData is None:
+ return False;
+
+ #
+ # Process the XML.
+ #
+ (sError, fUnforgivable) = TestResultLogic(oDb).processXmlStream(sXml, self._idTestSet);
+ if sError is not None:
+ oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
+ self.writeToMainLog(oTestSet, '\n!!XML error: %s\n%s\n\n' % (sError, sXml,));
+ if fUnforgivable:
+ return self._resultResponse(constants.tbresp.STATUS_NACK);
+ return self._resultResponse(constants.tbresp.STATUS_ACK);
+
+
+ def _actionExecCompleted(self):
+ """
+ Implement EXEC completion.
+
+ Because the action is request by the worker thread of the testbox
+ script we cannot pass pending commands back to it like originally
+ planned. So, we just complete the test set and update the status.
+ """
+ #
+ # Parameter validation.
+ #
+ sStatus = self._getStringParam(constants.tbreq.EXEC_COMPLETED_PARAM_RESULT, TestBoxController.kasValidResults);
+ self._checkForUnknownParameters();
+
+ (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
+ TestBoxStatusData.ksTestBoxState_GangTesting]);
+ if oStatusData is None:
+ return False;
+
+ #
+ # Complete the status.
+ #
+ oDb.rollback();
+ oDb.begin();
+ oTestSetLogic = TestSetLogic(oDb);
+ idTestSetGangLeader = oTestSetLogic.complete(oStatusData.idTestSet, self.kadTbResultToStatus[sStatus], fCommit = False);
+
+ oStatusLogic = TestBoxStatusLogic(oDb);
+ if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_Testing:
+ assert idTestSetGangLeader is None;
+ GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox);
+ oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
+ else:
+ assert idTestSetGangLeader is not None;
+ oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_GangCleanup, oStatusData.idTestSet,
+ fCommit = False);
+ if oStatusLogic.isWholeGangDoneTesting(idTestSetGangLeader):
+ GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox);
+ oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
+
+ oDb.commit();
+ return self._resultResponse(constants.tbresp.STATUS_ACK);
+
+
+
+ def _getStandardParams(self, dParams):
+ """
+ Gets the standard parameters and validates them.
+
+ The parameters are returned as a tuple: sAction, idTestBox, sTestBoxUuid.
+ Note! the sTextBoxId can be None if it's a SIGNON request.
+
+ Raises TestBoxControllerException on invalid input.
+ """
+ #
+ # Get the action parameter and validate it.
+ #
+ if constants.tbreq.ALL_PARAM_ACTION not in dParams:
+ raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
+ % (constants.tbreq.ALL_PARAM_ACTION, dParams,));
+ sAction = dParams[constants.tbreq.ALL_PARAM_ACTION];
+
+ if sAction not in self._dActions:
+ raise TestBoxControllerException('Unknown action "%s" in request (params: %s; action: %s)' \
+ % (sAction, dParams, self._dActions));
+
+ #
+ # TestBox UUID.
+ #
+ if constants.tbreq.ALL_PARAM_TESTBOX_UUID not in dParams:
+ raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
+ % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, dParams,));
+ sTestBoxUuid = dParams[constants.tbreq.ALL_PARAM_TESTBOX_UUID];
+ try:
+ sTestBoxUuid = str(uuid.UUID(sTestBoxUuid));
+ except Exception as oXcpt:
+ raise TestBoxControllerException('Invalid %s parameter value "%s": %s ' \
+ % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, sTestBoxUuid, oXcpt));
+ if sTestBoxUuid == '00000000-0000-0000-0000-000000000000':
+ raise TestBoxControllerException('Invalid %s parameter value "%s": NULL UUID not allowed.' \
+ % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, sTestBoxUuid));
+
+ #
+ # TestBox ID.
+ #
+ if constants.tbreq.ALL_PARAM_TESTBOX_ID in dParams:
+ sTestBoxId = dParams[constants.tbreq.ALL_PARAM_TESTBOX_ID];
+ try:
+ idTestBox = int(sTestBoxId);
+ if idTestBox <= 0 or idTestBox >= 0x7fffffff:
+ raise Exception;
+ except:
+ raise TestBoxControllerException('Bad value for "%s": "%s"' \
+ % (constants.tbreq.ALL_PARAM_TESTBOX_ID, sTestBoxId));
+ elif sAction == constants.tbreq.SIGNON:
+ idTestBox = None;
+ else:
+ raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
+ % (constants.tbreq.ALL_PARAM_TESTBOX_ID, dParams,));
+
+ #
+ # Test Set ID.
+ #
+ if constants.tbreq.RESULT_PARAM_TEST_SET_ID in dParams:
+ sTestSetId = dParams[constants.tbreq.RESULT_PARAM_TEST_SET_ID];
+ try:
+ idTestSet = int(sTestSetId);
+ if idTestSet <= 0 or idTestSet >= 0x7fffffff:
+ raise Exception;
+ except:
+ raise TestBoxControllerException('Bad value for "%s": "%s"' \
+ % (constants.tbreq.RESULT_PARAM_TEST_SET_ID, sTestSetId));
+ elif sAction not in [ constants.tbreq.XML_RESULTS, ]: ## More later.
+ idTestSet = None;
+ else:
+ raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
+ % (constants.tbreq.RESULT_PARAM_TEST_SET_ID, dParams,));
+
+ #
+ # The testbox address.
+ #
+ sTestBoxAddr = self._oSrvGlue.getClientAddr();
+ if sTestBoxAddr is None or sTestBoxAddr.strip() == '':
+ raise TestBoxControllerException('Invalid client address "%s"' % (sTestBoxAddr,));
+
+ #
+ # Update the list of checked parameters.
+ #
+ self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_TESTBOX_UUID, constants.tbreq.ALL_PARAM_ACTION]);
+ if idTestBox is not None:
+ self._asCheckedParams.append(constants.tbreq.ALL_PARAM_TESTBOX_ID);
+ if idTestSet is not None:
+ self._asCheckedParams.append(constants.tbreq.RESULT_PARAM_TEST_SET_ID);
+
+ return (sAction, idTestBox, sTestBoxUuid, sTestBoxAddr, idTestSet);
+
+ def dispatchRequest(self):
+ """
+ Dispatches the incoming request.
+
+ Will raise TestBoxControllerException on failure.
+ """
+
+ #
+ # Must be a POST request.
+ #
+ try:
+ sMethod = self._oSrvGlue.getMethod();
+ except Exception as oXcpt:
+ raise TestBoxControllerException('Error retriving request method: %s' % (oXcpt,));
+ if sMethod != 'POST':
+ raise TestBoxControllerException('Error expected POST request not "%s"' % (sMethod,));
+
+ #
+ # Get the parameters and checks for duplicates.
+ #
+ try:
+ dParams = self._oSrvGlue.getParameters();
+ except Exception as oXcpt:
+ raise TestBoxControllerException('Error retriving parameters: %s' % (oXcpt,));
+ for sKey in dParams.keys():
+ if len(dParams[sKey]) > 1:
+ raise TestBoxControllerException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey]));
+ dParams[sKey] = dParams[sKey][0];
+ self._dParams = dParams;
+
+ #
+ # Get+validate the standard action parameters and dispatch the request.
+ #
+ (self._sAction, self._idTestBox, self._sTestBoxUuid, self._sTestBoxAddr, self._idTestSet) = \
+ self._getStandardParams(dParams);
+ return self._dActions[self._sAction]();
diff --git a/src/VBox/ValidationKit/testmanager/core/testboxstatus.py b/src/VBox/ValidationKit/testmanager/core/testboxstatus.py
new file mode 100755
index 00000000..86c6041f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testboxstatus.py
@@ -0,0 +1,317 @@
+# -*- coding: utf-8 -*-
+# $Id: testboxstatus.py $
+
+"""
+Test Manager - TestBoxStatus.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMTooManyRows, TMRowNotFound;
+from testmanager.core.testbox import TestBoxData;
+
+
+class TestBoxStatusData(ModelDataBase):
+ """
+ TestBoxStatus Data.
+ """
+
+ ## @name TestBoxState_T
+ # @{
+ ksTestBoxState_Idle = 'idle';
+ ksTestBoxState_Testing = 'testing';
+ ksTestBoxState_GangGathering = 'gang-gathering';
+ ksTestBoxState_GangGatheringTimedOut = 'gang-gathering-timedout';
+ ksTestBoxState_GangTesting = 'gang-testing';
+ ksTestBoxState_GangCleanup = 'gang-cleanup';
+ ksTestBoxState_Rebooting = 'rebooting';
+ ksTestBoxState_Upgrading = 'upgrading';
+ ksTestBoxState_UpgradingAndRebooting = 'upgrading-and-rebooting';
+ ksTestBoxState_DoingSpecialCmd = 'doing-special-cmd';
+ ## @}
+
+ ksParam_idTestBox = 'TestBoxStatus_idTestBox';
+ ksParam_idGenTestBox = 'TestBoxStatus_idGenTestBox'
+ ksParam_tsUpdated = 'TestBoxStatus_tsUpdated';
+ ksParam_enmState = 'TestBoxStatus_enmState';
+ ksParam_idTestSet = 'TestBoxStatus_idTestSet';
+ ksParam_iWorkItem = 'TestBoxStatus_iWorkItem';
+
+ kasAllowNullAttributes = ['idTestSet', ];
+ kasValidValues_enmState = \
+ [
+ ksTestBoxState_Idle, ksTestBoxState_Testing, ksTestBoxState_GangGathering,
+ ksTestBoxState_GangGatheringTimedOut, ksTestBoxState_GangTesting, ksTestBoxState_GangCleanup,
+ ksTestBoxState_Rebooting, ksTestBoxState_Upgrading, ksTestBoxState_UpgradingAndRebooting,
+ ksTestBoxState_DoingSpecialCmd,
+ ];
+
+ kcDbColumns = 6;
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestBox = None;
+ self.idGenTestBox = None;
+ self.tsUpdated = None;
+ self.enmState = self.ksTestBoxState_Idle;
+ self.idTestSet = None;
+ self.iWorkItem = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Internal worker for initFromDbWithId and initFromDbWithGenId as well as
+ TestBoxStatusLogic.
+ """
+
+ if aoRow is None:
+ raise TMRowNotFound('TestBoxStatus not found.');
+
+ self.idTestBox = aoRow[0];
+ self.idGenTestBox = aoRow[1];
+ self.tsUpdated = aoRow[2];
+ self.enmState = aoRow[3];
+ self.idTestSet = aoRow[4];
+ self.iWorkItem = aoRow[5];
+ return self;
+
+ def initFromDbWithId(self, oDb, idTestBox):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute('SELECT *\n'
+ 'FROM TestBoxStatuses\n'
+ 'WHERE idTestBox = %s\n'
+ , (idTestBox, ) );
+ return self.initFromDbRow(oDb.fetchOne());
+
+ def initFromDbWithGenId(self, oDb, idGenTestBox):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute('SELECT *\n'
+ 'FROM TestBoxStatuses\n'
+ 'WHERE idGenTestBox = %s\n'
+ , (idGenTestBox, ) );
+ return self.initFromDbRow(oDb.fetchOne());
+
+
+class TestBoxStatusLogic(ModelLogicBase):
+ """
+ TestBoxStatus logic.
+ """
+
+ ## The number of seconds between each time to call touchStatus() when
+ # returning CMD_IDLE.
+ kcSecIdleTouchStatus = 120;
+
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb);
+
+
+ def tryFetchStatus(self, idTestBox):
+ """
+ Attempts to fetch the status of the given testbox.
+
+ Returns a TestBoxStatusData object on success.
+ Returns None if no status was found.
+ Raises exception on other errors.
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestBoxStatuses\n'
+ 'WHERE idTestBox = %s\n',
+ (idTestBox,));
+ if self._oDb.getRowCount() == 0:
+ return None;
+ oStatus = TestBoxStatusData();
+ return oStatus.initFromDbRow(self._oDb.fetchOne());
+
+ def tryFetchStatusAndConfig(self, idTestBox, sTestBoxUuid, sTestBoxAddr):
+ """
+ Tries to fetch the testbox status and current testbox config.
+
+ Returns (TestBoxStatusData, TestBoxData) on success, (None, None) if
+ not found. May throw an exception on database error.
+ """
+ self._oDb.execute('SELECT TestBoxStatuses.*,\n'
+ ' TestBoxesWithStrings.*\n'
+ 'FROM TestBoxStatuses,\n'
+ ' TestBoxesWithStrings\n'
+ 'WHERE TestBoxStatuses.idTestBox = %s\n'
+ ' AND TestBoxesWithStrings.idTestBox = %s\n'
+ ' AND TestBoxesWithStrings.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestBoxesWithStrings.uuidSystem = %s\n'
+ ' AND TestBoxesWithStrings.ip = %s\n'
+ , ( idTestBox,
+ idTestBox,
+ sTestBoxUuid,
+ sTestBoxAddr,) );
+ cRows = self._oDb.getRowCount();
+ if cRows != 1:
+ if cRows != 0:
+ raise TMTooManyRows('tryFetchStatusForCommandReq got %s rows for idTestBox=%s' % (cRows, idTestBox));
+ return (None, None);
+ aoRow = self._oDb.fetchOne();
+ return (TestBoxStatusData().initFromDbRow(aoRow[:TestBoxStatusData.kcDbColumns]),
+ TestBoxData().initFromDbRow(aoRow[TestBoxStatusData.kcDbColumns:]));
+
+
+ def insertIdleStatus(self, idTestBox, idGenTestBox, fCommit = False):
+ """
+ Inserts an idle status for the specified testbox.
+ """
+ self._oDb.execute('INSERT INTO TestBoxStatuses (\n'
+ ' idTestBox,\n'
+ ' idGenTestBox,\n'
+ ' enmState,\n'
+ ' idTestSet,\n'
+ ' iWorkItem)\n'
+ 'VALUES ( %s,\n'
+ ' %s,\n'
+ ' \'idle\'::TestBoxState_T,\n'
+ ' NULL,\n'
+ ' 0)\n'
+ , (idTestBox, idGenTestBox) );
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def touchStatus(self, idTestBox, fCommit = False):
+ """
+ Touches the testbox status row, i.e. sets tsUpdated to the current time.
+ """
+ self._oDb.execute('UPDATE TestBoxStatuses\n'
+ 'SET tsUpdated = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestBox = %s\n'
+ , (idTestBox,));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def updateState(self, idTestBox, sNewState, idTestSet = None, fCommit = False):
+ """
+ Updates the testbox state.
+ """
+ self._oDb.execute('UPDATE TestBoxStatuses\n'
+ 'SET enmState = %s,\n'
+ ' idTestSet = %s,\n'
+ ' tsUpdated = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestBox = %s\n',
+ (sNewState, idTestSet, idTestBox));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def updateGangStatus(self, idTestSetGangLeader, sNewState, fCommit = False):
+ """
+ Update the state of all members of a gang.
+ """
+ self._oDb.execute('UPDATE TestBoxStatuses\n'
+ 'SET enmState = %s,\n'
+ ' tsUpdated = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestBox IN (SELECT idTestBox\n'
+ ' FROM TestSets\n'
+ ' WHERE idTestSetGangLeader = %s)\n'
+ , (sNewState, idTestSetGangLeader,) );
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def updateWorkItem(self, idTestBox, iWorkItem, fCommit = False):
+ """
+ Updates the testbox state.
+ """
+ self._oDb.execute('UPDATE TestBoxStatuses\n'
+ 'SET iWorkItem = %s\n'
+ 'WHERE idTestBox = %s\n'
+ , ( iWorkItem, idTestBox,));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def isWholeGangDoneTesting(self, idTestSetGangLeader):
+ """
+ Checks if the whole gang is done testing.
+ """
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM TestBoxStatuses, TestSets\n'
+ 'WHERE TestBoxStatuses.idTestSet = TestSets.idTestSet\n'
+ ' AND TestSets.idTestSetGangLeader = %s\n'
+ ' AND TestBoxStatuses.enmState IN (%s, %s)\n'
+ , ( idTestSetGangLeader,
+ TestBoxStatusData.ksTestBoxState_GangGathering,
+ TestBoxStatusData.ksTestBoxState_GangTesting));
+ return self._oDb.fetchOne()[0] == 0;
+
+ def isTheWholeGangThere(self, idTestSetGangLeader):
+ """
+ Checks if the whole gang is done testing.
+ """
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM TestBoxStatuses, TestSets\n'
+ 'WHERE TestBoxStatuses.idTestSet = TestSets.idTestSet\n'
+ ' AND TestSets.idTestSetGangLeader = %s\n'
+ ' AND TestBoxStatuses.enmState IN (%s, %s)\n'
+ , ( idTestSetGangLeader,
+ TestBoxStatusData.ksTestBoxState_GangGathering,
+ TestBoxStatusData.ksTestBoxState_GangTesting));
+ return self._oDb.fetchOne()[0] == 0;
+
+ def timeSinceLastChangeInSecs(self, oStatusData):
+ """
+ Figures the time since the last status change.
+ """
+ tsNow = self._oDb.getCurrentTimestamp();
+ oDelta = tsNow - oStatusData.tsUpdated;
+ return oDelta.seconds + oDelta.days * 24 * 3600;
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class TestBoxStatusDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestBoxStatusData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testcase.pgsql b/src/VBox/ValidationKit/testmanager/core/testcase.pgsql
new file mode 100644
index 00000000..8d32d1c9
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testcase.pgsql
@@ -0,0 +1,275 @@
+-- $Id: testcase.pgsql $
+--- @file
+-- VBox Test Manager Database Stored Procedures - TestCases.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+\set ON_ERROR_STOP 1
+\connect testmanager;
+
+DROP FUNCTION IF EXISTS add_testcase(INTEGER, TEXT, TEXT, BOOLEAN, INTEGER, TEXT, TEXT);
+DROP FUNCTION IF EXISTS edit_testcase(INTEGER, INTEGER, TEXT, TEXT, BOOLEAN, INTEGER, TEXT, TEXT);
+DROP FUNCTION IF EXISTS del_testcase(INTEGER);
+DROP FUNCTION IF EXISTS TestCaseLogic_delEntry(INTEGER, INTEGER);
+DROP FUNCTION IF EXISTS TestCaseLogic_addEntry(a_uidAuthor INTEGER, a_sName TEXT, a_sDescription TEXT,
+ a_fEnabled BOOL, a_cSecTimeout INTEGER, a_sTestBoxReqExpr TEXT,
+ a_sBuildReqExpr TEXT, a_sBaseCmd TEXT, a_sTestSuiteZips TEXT);
+DROP FUNCTION IF EXISTS TestCaseLogic_editEntry(a_uidAuthor INTEGER, a_idTestCase INTEGER, a_sName TEXT, a_sDescription TEXT,
+ a_fEnabled BOOL, a_cSecTimeout INTEGER, a_sTestBoxReqExpr TEXT,
+ a_sBuildReqExpr TEXT, a_sBaseCmd TEXT, a_sTestSuiteZips TEXT);
+
+---
+-- Checks if the test case name is unique, ignoring a_idTestCaseIgnore.
+-- Raises exception if duplicates are found.
+--
+-- @internal
+--
+CREATE OR REPLACE FUNCTION TestCaseLogic_checkUniqueName(a_sName TEXT, a_idTestCaseIgnore INTEGER)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cRows INTEGER;
+ BEGIN
+ SELECT COUNT(*) INTO v_cRows
+ FROM TestCases
+ WHERE sName = a_sName
+ AND tsExpire = 'infinity'::TIMESTAMP
+ AND idTestCase <> a_idTestCaseIgnore;
+ IF v_cRows <> 0 THEN
+ RAISE EXCEPTION 'Duplicate test case name "%" (% times)', a_sName, v_cRows;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+---
+-- Check that the test case exists.
+-- Raises exception if it doesn't.
+--
+-- @internal
+--
+CREATE OR REPLACE FUNCTION TestCaseLogic_checkExists(a_idTestCase INTEGER) RETURNS VOID AS $$
+ BEGIN
+ IF NOT EXISTS( SELECT *
+ FROM TestCases
+ WHERE idTestCase = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP ) THEN
+ RAISE EXCEPTION 'Test case with ID % does not currently exist', a_idTestCase;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Historize a row.
+-- @internal
+--
+CREATE OR REPLACE FUNCTION TestCaseLogic_historizeEntry(a_idTestCase INTEGER, a_tsExpire TIMESTAMP WITH TIME ZONE)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cUpdatedRows INTEGER;
+ BEGIN
+ UPDATE TestCases
+ SET tsExpire = a_tsExpire
+ WHERE idTestcase = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP;
+ GET DIAGNOSTICS v_cUpdatedRows = ROW_COUNT;
+ IF v_cUpdatedRows <> 1 THEN
+ IF v_cUpdatedRows = 0 THEN
+ RAISE EXCEPTION 'Test case ID % does not currently exist', a_idTestCase;
+ END IF;
+ RAISE EXCEPTION 'Integrity error in TestCases: % current rows with idTestCase=%d', v_cUpdatedRows, a_idTestCase;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE function TestCaseLogic_addEntry(a_uidAuthor INTEGER, a_sName TEXT, a_sDescription TEXT,
+ a_fEnabled BOOL, a_cSecTimeout INTEGER, a_sTestBoxReqExpr TEXT,
+ a_sBuildReqExpr TEXT, a_sBaseCmd TEXT, a_sTestSuiteZips TEXT,
+ a_sComment TEXT)
+ RETURNS INTEGER AS $$
+ DECLARE
+ v_idTestCase INTEGER;
+ BEGIN
+ PERFORM TestCaseLogic_checkUniqueName(a_sName, -1);
+
+ INSERT INTO TestCases (uidAuthor, sName, sDescription, fEnabled, cSecTimeout,
+ sTestBoxReqExpr, sBuildReqExpr, sBaseCmd, sTestSuiteZips, sComment)
+ VALUES (a_uidAuthor, a_sName, a_sDescription, a_fEnabled, a_cSecTimeout,
+ a_sTestBoxReqExpr, a_sBuildReqExpr, a_sBaseCmd, a_sTestSuiteZips, a_sComment)
+ RETURNING idTestcase INTO v_idTestCase;
+ RETURN v_idTestCase;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE function TestCaseLogic_editEntry(a_uidAuthor INTEGER, a_idTestCase INTEGER, a_sName TEXT, a_sDescription TEXT,
+ a_fEnabled BOOL, a_cSecTimeout INTEGER, a_sTestBoxReqExpr TEXT,
+ a_sBuildReqExpr TEXT, a_sBaseCmd TEXT, a_sTestSuiteZips TEXT,
+ a_sComment TEXT)
+ RETURNS INTEGER AS $$
+ DECLARE
+ v_idGenTestCase INTEGER;
+ BEGIN
+ PERFORM TestCaseLogic_checkExists(a_idTestCase);
+ PERFORM TestCaseLogic_checkUniqueName(a_sName, a_idTestCase);
+
+ PERFORM TestCaseLogic_historizeEntry(a_idTestCase, CURRENT_TIMESTAMP);
+ INSERT INTO TestCases (idTestCase, uidAuthor, sName, sDescription, fEnabled, cSecTimeout,
+ sTestBoxReqExpr, sBuildReqExpr, sBaseCmd, sTestSuiteZips, sComment)
+ VALUES (a_idTestCase, a_uidAuthor, a_sName, a_sDescription, a_fEnabled, a_cSecTimeout,
+ a_sTestBoxReqExpr, a_sBuildReqExpr, a_sBaseCmd, a_sTestSuiteZips, a_sComment)
+ RETURNING idGenTestCase INTO v_idGenTestCase;
+ RETURN v_idGenTestCase;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION TestCaseLogic_delEntry(a_uidAuthor INTEGER, a_idTestCase INTEGER, a_fCascade BOOLEAN)
+ RETURNS VOID AS $$
+ DECLARE
+ v_Row TestCases%ROWTYPE;
+ v_tsEffective TIMESTAMP WITH TIME ZONE;
+ v_Rec RECORD;
+ v_sErrors TEXT;
+ BEGIN
+ --
+ -- Check preconditions.
+ --
+ IF a_fCascade <> TRUE THEN
+ IF EXISTS( SELECT *
+ FROM TestCaseDeps
+ WHERE idTestCasePreReq = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP ) THEN
+ v_sErrors := '';
+ FOR v_Rec IN
+ SELECT TestCases.idTestCase AS idTestCase,
+ TestCases.sName AS sName
+ FROM TestCaseDeps, TestCases
+ WHERE TestCaseDeps.idTestCasePreReq = a_idTestCase
+ AND TestCaseDeps.tsExpire = 'infinity'::TIMESTAMP
+ AND TestCases.idTestCase = TestCaseDeps.idTestCase
+ AND TestCases.tsExpire = 'infinity'::TIMESTAMP
+ LOOP
+ IF v_sErrors <> '' THEN
+ v_sErrors := v_sErrors || ', ';
+ END IF;
+ v_sErrors := v_sErrors || v_Rec.sName || ' (idTestCase=' || v_Rec.idTestCase || ')';
+ END LOOP;
+ RAISE EXCEPTION 'Other test cases depends on test case with ID %: % ', a_idTestCase, v_sErrors;
+ END IF;
+
+ IF EXISTS( SELECT *
+ FROM TestGroupMembers
+ WHERE idTestCase = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP ) THEN
+ v_sErrors := '';
+ FOR v_Rec IN
+ SELECT TestGroups.idTestGroup AS idTestGroup,
+ TestGroups.sName AS sName
+ FROM TestGroupMembers, TestGroups
+ WHERE TestGroupMembers.idTestCase = a_idTestCase
+ AND TestGroupMembers.tsExpire = 'infinity'::TIMESTAMP
+ AND TestGroupMembers.idTestGroup = TestGroups.idTestGroup
+ AND TestGroups.tsExpire = 'infinity'::TIMESTAMP
+ LOOP
+ IF v_sErrors <> '' THEN
+ v_sErrors := v_sErrors || ', ';
+ END IF;
+ v_sErrors := v_sErrors || v_Rec.sName || ' (idTestGroup=' || v_Rec.idTestGroup || ')';
+ END LOOP;
+ RAISE EXCEPTION 'Test case with ID % is member of the following test group(s): % ', a_idTestCase, v_sErrors;
+ END IF;
+ END IF;
+
+ --
+ -- To preserve the information about who deleted the record, we try to
+ -- add a dummy record which expires immediately. I say try because of
+ -- the primary key, we must let the new record be valid for 1 us. :-(
+ --
+ SELECT * INTO STRICT v_Row
+ FROM TestCases
+ WHERE idTestCase = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ v_tsEffective := CURRENT_TIMESTAMP - INTERVAL '1 microsecond';
+ IF v_Row.tsEffective < v_tsEffective THEN
+ PERFORM TestCaseLogic_historizeEntry(a_idTestCase, v_tsEffective);
+ v_Row.tsEffective := v_tsEffective;
+ v_Row.tsExpire := CURRENT_TIMESTAMP;
+ v_Row.uidAuthor := a_uidAuthor;
+ SELECT NEXTVAL('TestCaseGenIdSeq') INTO v_Row.idGenTestCase;
+ INSERT INTO TestCases VALUES (v_Row.*);
+ ELSE
+ PERFORM TestCaseLogic_historizeEntry(a_idTestCase, CURRENT_TIMESTAMP);
+ END IF;
+
+ --
+ -- Delete arguments, test case dependencies and resource dependencies.
+ -- (We don't bother recording who deleted the records here since it's
+ -- a lot of work and sufficiently covered in the TestCases table.)
+ --
+ UPDATE TestCaseArgs
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestCase = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ UPDATE TestCaseDeps
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestCase = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ UPDATE TestCaseGlobalRsrcDeps
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestCase = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ IF a_fCascade = TRUE THEN
+ UPDATE TestCaseDeps
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestCasePreReq = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ UPDATE TestGroupMembers
+ SET tsExpire = CURRENT_TIMESTAMP
+ WHERE idTestCase = a_idTestCase
+ AND tsExpire = 'infinity'::TIMESTAMP;
+ END IF;
+
+ EXCEPTION
+ WHEN NO_DATA_FOUND THEN
+ RAISE EXCEPTION 'Test case with ID % does not currently exist', a_idTestCase;
+ WHEN TOO_MANY_ROWS THEN
+ RAISE EXCEPTION 'Integrity error in TestCases: Too many current rows for %', a_idTestCase;
+ END;
+$$ LANGUAGE plpgsql;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testcase.py b/src/VBox/ValidationKit/testmanager/core/testcase.py
new file mode 100755
index 00000000..b2820ff2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testcase.py
@@ -0,0 +1,1467 @@
+# -*- coding: utf-8 -*-
+# $Id: testcase.py $
+# pylint: disable=too-many-lines
+
+"""
+Test Manager - Test Case.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import copy;
+import sys;
+import unittest;
+
+# Validation Kit imports.
+from common import utils;
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \
+ TMInvalidData, TMRowNotFound, ChangeLogEntry, AttributeChangeEntry;
+from testmanager.core.globalresource import GlobalResourceData;
+from testmanager.core.useraccount import UserAccountLogic;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ long = int; # pylint: disable=redefined-builtin,invalid-name
+
+
+
+class TestCaseGlobalRsrcDepData(ModelDataBase):
+ """
+ Test case dependency on a global resource - data.
+ """
+
+ ksParam_idTestCase = 'TestCaseDependency_idTestCase';
+ ksParam_idGlobalRsrc = 'TestCaseDependency_idGlobalRsrc';
+ ksParam_tsEffective = 'TestCaseDependency_tsEffective';
+ ksParam_tsExpire = 'TestCaseDependency_tsExpire';
+ ksParam_uidAuthor = 'TestCaseDependency_uidAuthor';
+
+ kasAllowNullAttributes = ['idTestSet', ];
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestCase = None;
+ self.idGlobalRsrc = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestCaseDeps row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test case not found.');
+
+ self.idTestCase = aoRow[0];
+ self.idGlobalRsrc = aoRow[1];
+ self.tsEffective = aoRow[2];
+ self.tsExpire = aoRow[3];
+ self.uidAuthor = aoRow[4];
+ return self;
+
+
+class TestCaseGlobalRsrcDepLogic(ModelLogicBase):
+ """
+ Test case dependency on a global resources - logic.
+ """
+
+ def getTestCaseDeps(self, idTestCase, tsNow = None):
+ """
+ Returns an array of (TestCaseGlobalRsrcDepData, GlobalResourceData)
+ with the global resources required by idTestCase.
+ Returns empty array if none found. Raises exception on database error.
+
+ Note! Maybe a bit overkill...
+ """
+ ## @todo This code isn't entirely kosher... Should use a DataEx with a oGlobalRsrc = GlobalResourceData().
+ if tsNow is not None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseGlobalRsrcDeps, GlobalResources\n'
+ 'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
+ ' AND TestCaseGlobalRsrcDeps.tsExpire > %s\n'
+ ' AND TestCaseGlobalRsrcDeps.tsEffective <= %s\n'
+ ' AND GlobalResources.idGlobalRsrc = TestCaseGlobalRsrcDeps.idGlobalRsrc\n'
+ ' AND GlobalResources.tsExpire > %s\n'
+ ' AND GlobalResources.tsEffective <= %s\n'
+ , (idTestCase, tsNow, tsNow, tsNow, tsNow) );
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseGlobalRsrcDeps, GlobalResources\n'
+ 'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
+ ' AND GlobalResources.idGlobalRsrc = TestCaseGlobalRsrcDeps.idGlobalRsrc\n'
+ ' AND TestCaseGlobalRsrcDeps.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND GlobalResources.tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestCase,))
+ aaoRows = self._oDb.fetchAll();
+ aoRet = []
+ for aoRow in aaoRows:
+ oItem = [TestCaseDependencyData().initFromDbRow(aoRow),
+ GlobalResourceData().initFromDbRow(aoRow[5:])];
+ aoRet.append(oItem);
+
+ return aoRet
+
+ def getTestCaseDepsIds(self, idTestCase, tsNow = None):
+ """
+ Returns an array of global resources that idTestCase require.
+ Returns empty array if none found. Raises exception on database error.
+ """
+ if tsNow is not None:
+ self._oDb.execute('SELECT idGlobalRsrc\n'
+ 'FROM TestCaseGlobalRsrcDeps\n'
+ 'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
+ ' AND TestCaseGlobalRsrcDeps.tsExpire > %s\n'
+ ' AND TestCaseGlobalRsrcDeps.tsEffective <= %s\n'
+ , (idTestCase, tsNow, tsNow, ) );
+ else:
+ self._oDb.execute('SELECT idGlobalRsrc\n'
+ 'FROM TestCaseGlobalRsrcDeps\n'
+ 'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
+ ' AND TestCaseGlobalRsrcDeps.tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestCase,))
+ aidGlobalRsrcs = []
+ for aoRow in self._oDb.fetchAll():
+ aidGlobalRsrcs.append(aoRow[0]);
+ return aidGlobalRsrcs;
+
+
+ def getDepGlobalResourceData(self, idTestCase, tsNow = None):
+ """
+ Returns an array of objects of type GlobalResourceData on which the
+ specified test case depends on.
+ """
+ if tsNow is None :
+ self._oDb.execute('SELECT GlobalResources.*\n'
+ 'FROM TestCaseGlobalRsrcDeps, GlobalResources\n'
+ 'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
+ ' AND GlobalResources.idGlobalRsrc = TestCaseGlobalRsrcDeps.idGlobalRsrc\n'
+ ' AND TestCaseGlobalRsrcDeps.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND GlobalResources.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY GlobalResources.idGlobalRsrc\n'
+ , (idTestCase,))
+ else:
+ self._oDb.execute('SELECT GlobalResources.*\n'
+ 'FROM TestCaseGlobalRsrcDeps, GlobalResources\n'
+ 'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
+ ' AND GlobalResources.idGlobalRsrc = TestCaseGlobalRsrcDeps.idGlobalRsrc\n'
+ ' AND TestCaseGlobalRsrcDeps.tsExpire > %s\n'
+ ' AND TestCaseGlobalRsrcDeps.tsExpire <= %s\n'
+ ' AND GlobalResources.tsExpire > %s\n'
+ ' AND GlobalResources.tsEffective <= %s\n'
+ 'ORDER BY GlobalResources.idGlobalRsrc\n'
+ , (idTestCase, tsNow, tsNow, tsNow, tsNow));
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(GlobalResourceData().initFromDbRow(aoRow));
+
+ return aoRet
+
+
+class TestCaseDependencyData(ModelDataBase):
+ """
+ Test case dependency data
+ """
+
+ ksParam_idTestCase = 'TestCaseDependency_idTestCase';
+ ksParam_idTestCasePreReq = 'TestCaseDependency_idTestCasePreReq';
+ ksParam_tsEffective = 'TestCaseDependency_tsEffective';
+ ksParam_tsExpire = 'TestCaseDependency_tsExpire';
+ ksParam_uidAuthor = 'TestCaseDependency_uidAuthor';
+
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestCase = None;
+ self.idTestCasePreReq = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestCaseDeps row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test case not found.');
+
+ self.idTestCase = aoRow[0];
+ self.idTestCasePreReq = aoRow[1];
+ self.tsEffective = aoRow[2];
+ self.tsExpire = aoRow[3];
+ self.uidAuthor = aoRow[4];
+ return self;
+
+ def initFromParams(self, oDisp, fStrict=True):
+ """
+ Initialize the object from parameters.
+ The input is not validated at all, except that all parameters must be
+ present when fStrict is True.
+ Note! Returns parameter NULL values, not database ones.
+ """
+
+ self.convertToParamNull();
+ fn = oDisp.getStringParam; # Shorter...
+
+ self.idTestCase = fn(self.ksParam_idTestCase, None, None if fStrict else self.idTestCase);
+ self.idTestCasePreReq = fn(self.ksParam_idTestCasePreReq, None, None if fStrict else self.idTestCasePreReq);
+ self.tsEffective = fn(self.ksParam_tsEffective, None, None if fStrict else self.tsEffective);
+ self.tsExpire = fn(self.ksParam_tsExpire, None, None if fStrict else self.tsExpire);
+ self.uidAuthor = fn(self.ksParam_uidAuthor, None, None if fStrict else self.uidAuthor);
+
+ return True
+
+ def validateAndConvert(self, oDb = None, enmValidateFor = ModelDataBase.ksValidateFor_Other):
+ """
+ Validates the input and converts valid fields to their right type.
+ Returns a dictionary with per field reports, only invalid fields will
+ be returned, so an empty dictionary means that the data is valid.
+
+ The dictionary keys are ksParam_*.
+ """
+ dErrors = {}
+
+ self.idTestCase = self._validateInt( dErrors, self.ksParam_idTestCase, self.idTestCase);
+ self.idTestCasePreReq = self._validateInt( dErrors, self.ksParam_idTestCasePreReq, self.idTestCasePreReq);
+ self.tsEffective = self._validateTs( dErrors, self.ksParam_tsEffective, self.tsEffective);
+ self.tsExpire = self._validateTs( dErrors, self.ksParam_tsExpire, self.tsExpire);
+ self.uidAuthor = self._validateInt( dErrors, self.ksParam_uidAuthor, self.uidAuthor);
+
+ _ = oDb;
+ _ = enmValidateFor;
+ return dErrors
+
+ def convertFromParamNull(self):
+ """
+ Converts from parameter NULL values to database NULL values (None).
+ """
+ if self.idTestCase in [-1, '']: self.idTestCase = None;
+ if self.idTestCasePreReq in [-1, '']: self.idTestCasePreReq = None;
+ if self.tsEffective == '': self.tsEffective = None;
+ if self.tsExpire == '': self.tsExpire = None;
+ if self.uidAuthor in [-1, '']: self.uidAuthor = None;
+ return True;
+
+ def convertToParamNull(self):
+ """
+ Converts from database NULL values (None) to special values we can
+ pass thru parameters list.
+ """
+ if self.idTestCase is None: self.idTestCase = -1;
+ if self.idTestCasePreReq is None: self.idTestCasePreReq = -1;
+ if self.tsEffective is None: self.tsEffective = '';
+ if self.tsExpire is None: self.tsExpire = '';
+ if self.uidAuthor is None: self.uidAuthor = -1;
+ return True;
+
+ def isEqual(self, oOther):
+ """ Compares two instances. """
+ return self.idTestCase == oOther.idTestCase \
+ and self.idTestCasePreReq == oOther.idTestCasePreReq \
+ and self.tsEffective == oOther.tsEffective \
+ and self.tsExpire == oOther.tsExpire \
+ and self.uidAuthor == oOther.uidAuthor;
+
+ def getTestCasePreReqIds(self, aTestCaseDependencyData):
+ """
+ Get list of Test Case IDs which current
+ Test Case depends on
+ """
+ if not aTestCaseDependencyData:
+ return []
+
+ aoRet = []
+ for oTestCaseDependencyData in aTestCaseDependencyData:
+ aoRet.append(oTestCaseDependencyData.idTestCasePreReq)
+
+ return aoRet
+
+class TestCaseDependencyLogic(ModelLogicBase):
+ """Test case dependency management logic"""
+
+ def getTestCaseDeps(self, idTestCase, tsEffective = None):
+ """
+ Returns an array of TestCaseDependencyData with the prerequisites of
+ idTestCase.
+ Returns empty array if none found. Raises exception on database error.
+ """
+ if tsEffective is not None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseDeps\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ , (idTestCase, tsEffective, tsEffective, ) );
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseDeps\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestCase, ) );
+ aaoRows = self._oDb.fetchAll();
+ aoRet = [];
+ for aoRow in aaoRows:
+ aoRet.append(TestCaseDependencyData().initFromDbRow(aoRow));
+
+ return aoRet
+
+ def getTestCaseDepsIds(self, idTestCase, tsNow = None):
+ """
+ Returns an array of test case IDs of the prerequisites of idTestCase.
+ Returns empty array if none found. Raises exception on database error.
+ """
+ if tsNow is not None:
+ self._oDb.execute('SELECT idTestCase\n'
+ 'FROM TestCaseDeps\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ , (idTestCase, tsNow, tsNow, ) );
+ else:
+ self._oDb.execute('SELECT idTestCase\n'
+ 'FROM TestCaseDeps\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestCase, ) );
+ aidPreReqs = [];
+ for aoRow in self._oDb.fetchAll():
+ aidPreReqs.append(aoRow[0]);
+ return aidPreReqs;
+
+
+ def getDepTestCaseData(self, idTestCase, tsNow = None):
+ """
+ Returns an array of objects of type TestCaseData2 on which
+ specified test case depends on
+ """
+ if tsNow is None:
+ self._oDb.execute('SELECT TestCases.*\n'
+ 'FROM TestCases, TestCaseDeps\n'
+ 'WHERE TestCaseDeps.idTestCase = %s\n'
+ ' AND TestCaseDeps.idTestCasePreReq = TestCases.idTestCase\n'
+ ' AND TestCaseDeps.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestCases.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY TestCases.idTestCase\n'
+ , (idTestCase, ) );
+ else:
+ self._oDb.execute('SELECT TestCases.*\n'
+ 'FROM TestCases, TestCaseDeps\n'
+ 'WHERE TestCaseDeps.idTestCase = %s\n'
+ ' AND TestCaseDeps.idTestCasePreReq = TestCases.idTestCase\n'
+ ' AND TestCaseDeps.tsExpire > %s\n'
+ ' AND TestCaseDeps.tsEffective <= %s\n'
+ ' AND TestCases.tsExpire > %s\n'
+ ' AND TestCases.tsEffective <= %s\n'
+ 'ORDER BY TestCases.idTestCase\n'
+ , (idTestCase, tsNow, tsNow, tsNow, tsNow, ) );
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(TestCaseData().initFromDbRow(aoRow));
+
+ return aoRet
+
+ def getApplicableDepTestCaseData(self, idTestCase):
+ """
+ Returns an array of objects of type TestCaseData on which
+ specified test case might depends on (all test
+ cases except the specified one and those testcases which are
+ depend on idTestCase)
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCases\n'
+ 'WHERE idTestCase <> %s\n'
+ ' AND idTestCase NOT IN (SELECT idTestCase\n'
+ ' FROM TestCaseDeps\n'
+ ' WHERE idTestCasePreReq=%s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP)\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestCase, idTestCase) )
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(TestCaseData().initFromDbRow(aoRow));
+
+ return aoRet
+
+class TestCaseData(ModelDataBase):
+ """
+ Test case data
+ """
+
+ ksIdAttr = 'idTestCase';
+ ksIdGenAttr = 'idGenTestCase';
+
+ ksParam_idTestCase = 'TestCase_idTestCase'
+ ksParam_tsEffective = 'TestCase_tsEffective'
+ ksParam_tsExpire = 'TestCase_tsExpire'
+ ksParam_uidAuthor = 'TestCase_uidAuthor'
+ ksParam_idGenTestCase = 'TestCase_idGenTestCase'
+ ksParam_sName = 'TestCase_sName'
+ ksParam_sDescription = 'TestCase_sDescription'
+ ksParam_fEnabled = 'TestCase_fEnabled'
+ ksParam_cSecTimeout = 'TestCase_cSecTimeout'
+ ksParam_sTestBoxReqExpr = 'TestCase_sTestBoxReqExpr';
+ ksParam_sBuildReqExpr = 'TestCase_sBuildReqExpr';
+ ksParam_sBaseCmd = 'TestCase_sBaseCmd'
+ ksParam_sValidationKitZips = 'TestCase_sValidationKitZips'
+ ksParam_sComment = 'TestCase_sComment'
+
+ kasAllowNullAttributes = [ 'idTestCase', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestCase', 'sDescription',
+ 'sTestBoxReqExpr', 'sBuildReqExpr', 'sValidationKitZips', 'sComment' ];
+
+ kcDbColumns = 14;
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestCase = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.idGenTestCase = None;
+ self.sName = None;
+ self.sDescription = None;
+ self.fEnabled = False;
+ self.cSecTimeout = 10; # Init with minimum timeout value
+ self.sTestBoxReqExpr = None;
+ self.sBuildReqExpr = None;
+ self.sBaseCmd = None;
+ self.sValidationKitZips = None;
+ self.sComment = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestCases row.
+ Returns self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test case not found.');
+
+ self.idTestCase = aoRow[0];
+ self.tsEffective = aoRow[1];
+ self.tsExpire = aoRow[2];
+ self.uidAuthor = aoRow[3];
+ self.idGenTestCase = aoRow[4];
+ self.sName = aoRow[5];
+ self.sDescription = aoRow[6];
+ self.fEnabled = aoRow[7];
+ self.cSecTimeout = aoRow[8];
+ self.sTestBoxReqExpr = aoRow[9];
+ self.sBuildReqExpr = aoRow[10];
+ self.sBaseCmd = aoRow[11];
+ self.sValidationKitZips = aoRow[12];
+ self.sComment = aoRow[13];
+ return self;
+
+ def initFromDbWithId(self, oDb, idTestCase, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM TestCases\n'
+ 'WHERE idTestCase = %s\n'
+ , ( idTestCase,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idTestCase=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestCase, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+ def initFromDbWithGenId(self, oDb, idGenTestCase, tsNow = None):
+ """
+ Initialize the object from the database.
+ """
+ _ = tsNow; # For relevant for the TestCaseDataEx version only.
+ oDb.execute('SELECT *\n'
+ 'FROM TestCases\n'
+ 'WHERE idGenTestCase = %s\n'
+ , (idGenTestCase, ) );
+ return self.initFromDbRow(oDb.fetchOne());
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ if sAttr == 'cSecTimeout' and oValue not in aoNilValues: # Allow human readable interval formats.
+ return utils.parseIntervalSeconds(oValue);
+
+ (oValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+ if sError is None:
+ if sAttr == 'sTestBoxReqExpr':
+ sError = TestCaseData.validateTestBoxReqExpr(oValue);
+ elif sAttr == 'sBuildReqExpr':
+ sError = TestCaseData.validateBuildReqExpr(oValue);
+ elif sAttr == 'sBaseCmd':
+ _, sError = TestCaseData.validateStr(oValue, fAllowUnicodeSymbols=False);
+ return (oValue, sError);
+
+
+ #
+ # Misc.
+ #
+
+ def needValidationKitBit(self):
+ """
+ Predicate method for checking whether a validation kit build is required.
+ """
+ return self.sValidationKitZips is None \
+ or self.sValidationKitZips.find('@VALIDATIONKIT_ZIP@') >= 0;
+
+ def matchesTestBoxProps(self, oTestBoxData):
+ """
+ Checks if the all of the testbox related test requirements matches the
+ given testbox.
+
+ Returns True or False according to the expression, None on exception or
+ non-boolean expression result.
+ """
+ return TestCaseData.matchesTestBoxPropsEx(oTestBoxData, self.sTestBoxReqExpr);
+
+ def matchesBuildProps(self, oBuildDataEx):
+ """
+ Checks if the all of the build related test requirements matches the
+ given build.
+
+ Returns True or False according to the expression, None on exception or
+ non-boolean expression result.
+ """
+ return TestCaseData.matchesBuildPropsEx(oBuildDataEx, self.sBuildReqExpr);
+
+
+ #
+ # Expression validation code shared with TestCaseArgsDataEx.
+ #
+ @staticmethod
+ def _safelyEvalExpr(sExpr, dLocals, fMayRaiseXcpt = False):
+ """
+ Safely evaluate requirment expression given a set of locals.
+
+ Returns True or False according to the expression. If the expression
+ causes an exception to be raised or does not return a boolean result,
+ None will be returned.
+ """
+ if sExpr is None or sExpr == '':
+ return True;
+
+ dGlobals = \
+ {
+ '__builtins__': None,
+ 'long': long,
+ 'int': int,
+ 'bool': bool,
+ 'True': True,
+ 'False': False,
+ 'len': len,
+ 'isinstance': isinstance,
+ 'type': type,
+ 'dict': dict,
+ 'dir': dir,
+ 'list': list,
+ 'versionCompare': utils.versionCompare,
+ };
+
+ try:
+ fRc = eval(sExpr, dGlobals, dLocals);
+ except:
+ if fMayRaiseXcpt:
+ raise;
+ return None;
+
+ if not isinstance(fRc, bool):
+ if fMayRaiseXcpt:
+ raise Exception('not a boolean result: "%s" - %s' % (fRc, type(fRc)) );
+ return None;
+
+ return fRc;
+
+ @staticmethod
+ def _safelyValidateReqExpr(sExpr, adLocals):
+ """
+ Validates a requirement expression using the given sets of locals,
+ returning None on success and an error string on failure.
+ """
+ for dLocals in adLocals:
+ try:
+ TestCaseData._safelyEvalExpr(sExpr, dLocals, True);
+ except Exception as oXcpt:
+ return str(oXcpt);
+ return None;
+
+ @staticmethod
+ def validateTestBoxReqExpr(sExpr):
+ """
+ Validates a testbox expression, returning None on success and an error
+ string on failure.
+ """
+ adTestBoxes = \
+ [
+ {
+ 'sOs': 'win',
+ 'sOsVersion': '3.1',
+ 'sCpuVendor': 'VirtualBox',
+ 'sCpuArch': 'x86',
+ 'cCpus': 1,
+ 'fCpuHwVirt': False,
+ 'fCpuNestedPaging': False,
+ 'fCpu64BitGuest': False,
+ 'fChipsetIoMmu': False,
+ 'fRawMode': False,
+ 'cMbMemory': 985034,
+ 'cMbScratch': 1234089,
+ 'iTestBoxScriptRev': 1,
+ 'sName': 'emanon',
+ 'uuidSystem': '8FF81BE5-3901-4AB1-8A65-B48D511C0321',
+ },
+ {
+ 'sOs': 'linux',
+ 'sOsVersion': '3.1',
+ 'sCpuVendor': 'VirtualBox',
+ 'sCpuArch': 'amd64',
+ 'cCpus': 8191,
+ 'fCpuHwVirt': True,
+ 'fCpuNestedPaging': True,
+ 'fCpu64BitGuest': True,
+ 'fChipsetIoMmu': True,
+ 'fRawMode': True,
+ 'cMbMemory': 9999999999,
+ 'cMbScratch': 9999999999999,
+ 'iTestBoxScriptRev': 9999999,
+ 'sName': 'emanon',
+ 'uuidSystem': '00000000-0000-0000-0000-000000000000',
+ },
+ ];
+ return TestCaseData._safelyValidateReqExpr(sExpr, adTestBoxes);
+
+ @staticmethod
+ def matchesTestBoxPropsEx(oTestBoxData, sExpr):
+ """ Worker for TestCaseData.matchesTestBoxProps and TestCaseArgsDataEx.matchesTestBoxProps. """
+ if sExpr is None:
+ return True;
+ dLocals = \
+ {
+ 'sOs': oTestBoxData.sOs,
+ 'sOsVersion': oTestBoxData.sOsVersion,
+ 'sCpuVendor': oTestBoxData.sCpuVendor,
+ 'sCpuArch': oTestBoxData.sCpuArch,
+ 'iCpuFamily': oTestBoxData.getCpuFamily(),
+ 'iCpuModel': oTestBoxData.getCpuModel(),
+ 'cCpus': oTestBoxData.cCpus,
+ 'fCpuHwVirt': oTestBoxData.fCpuHwVirt,
+ 'fCpuNestedPaging': oTestBoxData.fCpuNestedPaging,
+ 'fCpu64BitGuest': oTestBoxData.fCpu64BitGuest,
+ 'fChipsetIoMmu': oTestBoxData.fChipsetIoMmu,
+ 'fRawMode': oTestBoxData.fRawMode,
+ 'cMbMemory': oTestBoxData.cMbMemory,
+ 'cMbScratch': oTestBoxData.cMbScratch,
+ 'iTestBoxScriptRev': oTestBoxData.iTestBoxScriptRev,
+ 'iPythonHexVersion': oTestBoxData.iPythonHexVersion,
+ 'sName': oTestBoxData.sName,
+ 'uuidSystem': oTestBoxData.uuidSystem,
+ };
+ return TestCaseData._safelyEvalExpr(sExpr, dLocals);
+
+ @staticmethod
+ def validateBuildReqExpr(sExpr):
+ """
+ Validates a testbox expression, returning None on success and an error
+ string on failure.
+ """
+ adBuilds = \
+ [
+ {
+ 'sProduct': 'VirtualBox',
+ 'sBranch': 'trunk',
+ 'sType': 'release',
+ 'asOsArches': ['win.amd64', 'win.x86'],
+ 'sVersion': '1.0',
+ 'iRevision': 1234,
+ 'uidAuthor': None,
+ 'idBuild': 953,
+ },
+ {
+ 'sProduct': 'VirtualBox',
+ 'sBranch': 'VBox-4.1',
+ 'sType': 'release',
+ 'asOsArches': ['linux.x86',],
+ 'sVersion': '4.2.15',
+ 'iRevision': 89876,
+ 'uidAuthor': None,
+ 'idBuild': 945689,
+ },
+ {
+ 'sProduct': 'VirtualBox',
+ 'sBranch': 'VBox-4.1',
+ 'sType': 'strict',
+ 'asOsArches': ['solaris.x86', 'solaris.amd64',],
+ 'sVersion': '4.3.0_RC3',
+ 'iRevision': 97939,
+ 'uidAuthor': 33,
+ 'idBuild': 9456893,
+ },
+ ];
+ return TestCaseData._safelyValidateReqExpr(sExpr, adBuilds);
+
+ @staticmethod
+ def matchesBuildPropsEx(oBuildDataEx, sExpr):
+ """
+ Checks if the all of the build related test requirements matches the
+ given build.
+ """
+ if sExpr is None:
+ return True;
+ dLocals = \
+ {
+ 'sProduct': oBuildDataEx.oCat.sProduct,
+ 'sBranch': oBuildDataEx.oCat.sBranch,
+ 'sType': oBuildDataEx.oCat.sType,
+ 'asOsArches': oBuildDataEx.oCat.asOsArches,
+ 'sVersion': oBuildDataEx.sVersion,
+ 'iRevision': oBuildDataEx.iRevision,
+ 'uidAuthor': oBuildDataEx.uidAuthor,
+ 'idBuild': oBuildDataEx.idBuild,
+ };
+ return TestCaseData._safelyEvalExpr(sExpr, dLocals);
+
+
+
+
+class TestCaseDataEx(TestCaseData):
+ """
+ Test case data.
+ """
+
+ ksParam_aoTestCaseArgs = 'TestCase_aoTestCaseArgs';
+ ksParam_aoDepTestCases = 'TestCase_aoDepTestCases';
+ ksParam_aoDepGlobalResources = 'TestCase_aoDepGlobalResources';
+
+ # Use [] instead of None.
+ kasAltArrayNull = [ 'aoTestCaseArgs', 'aoDepTestCases', 'aoDepGlobalResources' ];
+
+
+ def __init__(self):
+ TestCaseData.__init__(self);
+
+ # List of objects of type TestCaseData (or TestCaseDataEx, we don't
+ # care) on which current Test Case depends.
+ self.aoDepTestCases = [];
+
+ # List of objects of type GlobalResourceData on which current Test Case depends.
+ self.aoDepGlobalResources = [];
+
+ # List of objects of type TestCaseArgsData.
+ self.aoTestCaseArgs = [];
+
+ def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None):
+ """
+ Worker shared by the initFromDb* methods.
+ Returns self. Raises exception if no row or database error.
+ """
+ _ = sPeriodBack; ## @todo sPeriodBack
+ from testmanager.core.testcaseargs import TestCaseArgsLogic;
+ self.aoDepTestCases = TestCaseDependencyLogic(oDb).getDepTestCaseData(self.idTestCase, tsNow);
+ self.aoDepGlobalResources = TestCaseGlobalRsrcDepLogic(oDb).getDepGlobalResourceData(self.idTestCase, tsNow);
+ self.aoTestCaseArgs = TestCaseArgsLogic(oDb).getTestCaseArgs(self.idTestCase, tsNow);
+ # Note! The above arrays are sorted by their relvant IDs for fetchForChangeLog's sake.
+ return self;
+
+ def initFromDbRowEx(self, aoRow, oDb, tsNow = None):
+ """
+ Reinitialize from a SELECT * FROM TestCases row. Will query the
+ necessary additional data from oDb using tsNow.
+ Returns self. Raises exception if no row or database error.
+ """
+ TestCaseData.initFromDbRow(self, aoRow);
+ return self._initExtraMembersFromDb(oDb, tsNow);
+
+ def initFromDbWithId(self, oDb, idTestCase, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ TestCaseData.initFromDbWithId(self, oDb, idTestCase, tsNow, sPeriodBack);
+ return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack);
+
+ def initFromDbWithGenId(self, oDb, idGenTestCase, tsNow = None):
+ """
+ Initialize the object from the database.
+ """
+ TestCaseData.initFromDbWithGenId(self, oDb, idGenTestCase);
+ if tsNow is None and not oDb.isTsInfinity(self.tsExpire):
+ tsNow = self.tsEffective;
+ return self._initExtraMembersFromDb(oDb, tsNow);
+
+ def getAttributeParamNullValues(self, sAttr):
+ if sAttr in ['aoDepTestCases', 'aoDepGlobalResources', 'aoTestCaseArgs']:
+ return [[], ''];
+ return TestCaseData.getAttributeParamNullValues(self, sAttr);
+
+ def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
+ """For dealing with the arrays."""
+ if sAttr not in ['aoDepTestCases', 'aoDepGlobalResources', 'aoTestCaseArgs']:
+ return TestCaseData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict);
+
+ aoNewValues = [];
+ if sAttr == 'aoDepTestCases':
+ for idTestCase in oDisp.getListOfIntParams(sParam, 1, 0x7ffffffe, []):
+ oDep = TestCaseData();
+ oDep.idTestCase = str(idTestCase);
+ aoNewValues.append(oDep);
+
+ elif sAttr == 'aoDepGlobalResources':
+ for idGlobalRsrc in oDisp.getListOfIntParams(sParam, 1, 0x7ffffffe, []):
+ oGlobalRsrc = GlobalResourceData();
+ oGlobalRsrc.idGlobalRsrc = str(idGlobalRsrc);
+ aoNewValues.append(oGlobalRsrc);
+
+ elif sAttr == 'aoTestCaseArgs':
+ from testmanager.core.testcaseargs import TestCaseArgsData;
+ for sArgKey in oDisp.getStringParam(TestCaseDataEx.ksParam_aoTestCaseArgs, sDefault = '').split(','):
+ oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestCaseDataEx.ksParam_aoTestCaseArgs, sArgKey,))
+ aoNewValues.append(TestCaseArgsData().initFromParams(oDispWrapper, fStrict = False));
+ return aoNewValues;
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): # pylint: disable=too-many-locals
+ """
+ Validate special arrays and requirement expressions.
+
+ For the two dependency arrays we have to supply missing bits by
+ looking them up in the database. In the argument variation case we
+ need to validate each item.
+ """
+ if sAttr not in ['aoDepTestCases', 'aoDepGlobalResources', 'aoTestCaseArgs']:
+ return TestCaseData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+
+ asErrors = [];
+ aoNewValues = [];
+ if sAttr == 'aoDepTestCases':
+ for oTestCase in self.aoDepTestCases:
+ if utils.isString(oTestCase.idTestCase): # Stored as string convertParamToAttribute.
+ oTestCase = copy.copy(oTestCase);
+ try:
+ oTestCase.idTestCase = int(oTestCase.idTestCase);
+ oTestCase.initFromDbWithId(oDb, oTestCase.idTestCase);
+ except Exception as oXcpt:
+ asErrors.append('Test case dependency #%s: %s' % (oTestCase.idTestCase, oXcpt));
+ aoNewValues.append(oTestCase);
+
+ elif sAttr == 'aoDepGlobalResources':
+ for oGlobalRsrc in self.aoDepGlobalResources:
+ if utils.isString(oGlobalRsrc.idGlobalRsrc): # Stored as string convertParamToAttribute.
+ oGlobalRsrc = copy.copy(oGlobalRsrc);
+ try:
+ oGlobalRsrc.idTestCase = int(oGlobalRsrc.idGlobalRsrc);
+ oGlobalRsrc.initFromDbWithId(oDb, oGlobalRsrc.idGlobalRsrc);
+ except Exception as oXcpt:
+ asErrors.append('Resource dependency #%s: %s' % (oGlobalRsrc.idGlobalRsrc, oXcpt));
+ aoNewValues.append(oGlobalRsrc);
+
+ else:
+ assert sAttr == 'aoTestCaseArgs';
+ if not self.aoTestCaseArgs:
+ return (None, 'The testcase requires at least one argument variation to be valid.');
+
+ # Note! We'll be returning an error dictionary instead of an string here.
+ dErrors = {};
+
+ for iVar, oVar in enumerate(self.aoTestCaseArgs):
+ oVar = copy.copy(oVar);
+ oVar.idTestCase = self.idTestCase;
+ dCurErrors = oVar.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other);
+ if not dCurErrors:
+ pass; ## @todo figure out the ID?
+ else:
+ asErrors = [];
+ for sKey in dCurErrors:
+ asErrors.append('%s: %s' % (sKey[len('TestCaseArgs_'):], dCurErrors[sKey]));
+ dErrors[iVar] = '<br>\n'.join(asErrors)
+ aoNewValues.append(oVar);
+
+ for iVar, oVar in enumerate(self.aoTestCaseArgs):
+ sArgs = oVar.sArgs;
+ for iVar2 in range(iVar + 1, len(self.aoTestCaseArgs)):
+ if self.aoTestCaseArgs[iVar2].sArgs == sArgs:
+ sMsg = 'Duplicate argument variation "%s".' % (sArgs);
+ if iVar in dErrors: dErrors[iVar] += '<br>\n' + sMsg;
+ else: dErrors[iVar] = sMsg;
+ if iVar2 in dErrors: dErrors[iVar2] += '<br>\n' + sMsg;
+ else: dErrors[iVar2] = sMsg;
+ break;
+
+ return (aoNewValues, dErrors if dErrors else None);
+
+ return (aoNewValues, None if not asErrors else ' <br>'.join(asErrors));
+
+ def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other):
+ dErrors = TestCaseData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor);
+
+ # Validate dependencies a wee bit for paranoid reasons. The scheduler
+ # queue generation code does the real validation here!
+ if not dErrors and self.idTestCase is not None:
+ for oDep in self.aoDepTestCases:
+ if oDep.idTestCase == self.idTestCase:
+ if self.ksParam_aoDepTestCases in dErrors:
+ dErrors[self.ksParam_aoDepTestCases] += ' Depending on itself!';
+ else:
+ dErrors[self.ksParam_aoDepTestCases] = 'Depending on itself!';
+ return dErrors;
+
+
+
+
+
+class TestCaseLogic(ModelLogicBase):
+ """
+ Test case management logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ def getAll(self):
+ """
+ Fetches all test case records from DB (TestCaseData).
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCases\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY idTestCase ASC;')
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = [];
+ for aoRow in aaoRows:
+ aoRet.append(TestCaseData().initFromDbRow(aoRow))
+ return aoRet
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches test cases.
+
+ Returns an array (list) of TestCaseDataEx items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCases\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sName ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart, ));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCases\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY sName ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart, ));
+
+ aoRows = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(TestCaseDataEx().initFromDbRowEx(aoRow, self._oDb, tsNow));
+ return aoRows;
+
+ def fetchForChangeLog(self, idTestCase, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals
+ """
+ Fetches change log entries for a testbox.
+
+ Returns an array of ChangeLogEntry instance and an indicator whether
+ there are more entries.
+ Raises exception on error.
+ """
+
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+
+ # 1. Get a list of the relevant change times.
+ self._oDb.execute('( SELECT tsEffective, uidAuthor FROM TestCases WHERE idTestCase = %s AND tsEffective <= %s )\n'
+ 'UNION\n'
+ '( SELECT tsEffective, uidAuthor FROM TestCaseArgs WHERE idTestCase = %s AND tsEffective <= %s )\n'
+ 'UNION\n'
+ '( SELECT tsEffective, uidAuthor FROM TestCaseDeps WHERE idTestCase = %s AND tsEffective <= %s )\n'
+ 'UNION\n'
+ '( SELECT tsEffective, uidAuthor FROM TestCaseGlobalRsrcDeps \n' \
+ ' WHERE idTestCase = %s AND tsEffective <= %s )\n'
+ 'ORDER BY tsEffective DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , ( idTestCase, tsNow,
+ idTestCase, tsNow,
+ idTestCase, tsNow,
+ idTestCase, tsNow,
+ cMaxRows + 1, iStart, ));
+ aaoChanges = self._oDb.fetchAll();
+
+ # 2. Collect data sets for each of those points.
+ # (Doing it the lazy + inefficient way for now.)
+ aoRows = [];
+ for aoChange in aaoChanges:
+ aoRows.append(TestCaseDataEx().initFromDbWithId(self._oDb, idTestCase, aoChange[0]));
+
+ # 3. Calculate the changes.
+ aoEntries = [];
+ for i in range(0, len(aoRows) - 1):
+ oNew = aoRows[i];
+ oOld = aoRows[i + 1];
+ (tsEffective, uidAuthor) = aaoChanges[i];
+ (tsExpire, _) = aaoChanges[i - 1] if i > 0 else (oNew.tsExpire, None)
+ assert self._oDb.isTsInfinity(tsEffective) != self._oDb.isTsInfinity(tsExpire) or tsEffective < tsExpire, \
+ '%s vs %s' % (tsEffective, tsExpire);
+
+ aoChanges = [];
+
+ # The testcase object.
+ if oNew.tsEffective != oOld.tsEffective:
+ for sAttr in oNew.getDataAttributes():
+ if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', \
+ 'aoTestCaseArgs', 'aoDepTestCases', 'aoDepGlobalResources']:
+ oOldAttr = getattr(oOld, sAttr);
+ oNewAttr = getattr(oNew, sAttr);
+ if oOldAttr != oNewAttr:
+ aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+
+ # The argument variations.
+ iChildOld = 0;
+ for oChildNew in oNew.aoTestCaseArgs:
+ # Locate the old entry, emitting removed markers for old items we have to skip.
+ while iChildOld < len(oOld.aoTestCaseArgs) \
+ and oOld.aoTestCaseArgs[iChildOld].idTestCaseArgs < oChildNew.idTestCaseArgs:
+ oChildOld = oOld.aoTestCaseArgs[iChildOld];
+ aoChanges.append(AttributeChangeEntry('Variation #%s' % (oChildOld.idTestCaseArgs,),
+ None, oChildOld, 'Removed', str(oChildOld)));
+ iChildOld += 1;
+
+ if iChildOld < len(oOld.aoTestCaseArgs) \
+ and oOld.aoTestCaseArgs[iChildOld].idTestCaseArgs == oChildNew.idTestCaseArgs:
+ oChildOld = oOld.aoTestCaseArgs[iChildOld];
+ if oChildNew.tsEffective != oChildOld.tsEffective:
+ for sAttr in oChildNew.getDataAttributes():
+ if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestCase', ]:
+ oOldAttr = getattr(oChildOld, sAttr);
+ oNewAttr = getattr(oChildNew, sAttr);
+ if oOldAttr != oNewAttr:
+ aoChanges.append(AttributeChangeEntry('Variation[#%s].%s'
+ % (oChildOld.idTestCaseArgs, sAttr,),
+ oNewAttr, oOldAttr,
+ str(oNewAttr), str(oOldAttr)));
+ iChildOld += 1;
+ else:
+ aoChanges.append(AttributeChangeEntry('Variation #%s' % (oChildNew.idTestCaseArgs,),
+ oChildNew, None,
+ str(oChildNew), 'Did not exist'));
+
+ # The testcase dependencies.
+ iChildOld = 0;
+ for oChildNew in oNew.aoDepTestCases:
+ # Locate the old entry, emitting removed markers for old items we have to skip.
+ while iChildOld < len(oOld.aoDepTestCases) \
+ and oOld.aoDepTestCases[iChildOld].idTestCase < oChildNew.idTestCase:
+ oChildOld = oOld.aoDepTestCases[iChildOld];
+ aoChanges.append(AttributeChangeEntry('Dependency #%s' % (oChildOld.idTestCase,),
+ None, oChildOld, 'Removed',
+ '%s (#%u)' % (oChildOld.sName, oChildOld.idTestCase,)));
+ iChildOld += 1;
+ if iChildOld < len(oOld.aoDepTestCases) \
+ and oOld.aoDepTestCases[iChildOld].idTestCase == oChildNew.idTestCase:
+ iChildOld += 1;
+ else:
+ aoChanges.append(AttributeChangeEntry('Dependency #%s' % (oChildNew.idTestCase,),
+ oChildNew, None,
+ '%s (#%u)' % (oChildNew.sName, oChildNew.idTestCase,),
+ 'Did not exist'));
+
+ # The global resource dependencies.
+ iChildOld = 0;
+ for oChildNew in oNew.aoDepGlobalResources:
+ # Locate the old entry, emitting removed markers for old items we have to skip.
+ while iChildOld < len(oOld.aoDepGlobalResources) \
+ and oOld.aoDepGlobalResources[iChildOld].idGlobalRsrc < oChildNew.idGlobalRsrc:
+ oChildOld = oOld.aoDepGlobalResources[iChildOld];
+ aoChanges.append(AttributeChangeEntry('Global Resource #%s' % (oChildOld.idGlobalRsrc,),
+ None, oChildOld, 'Removed',
+ '%s (#%u)' % (oChildOld.sName, oChildOld.idGlobalRsrc,)));
+ iChildOld += 1;
+ if iChildOld < len(oOld.aoDepGlobalResources) \
+ and oOld.aoDepGlobalResources[iChildOld].idGlobalRsrc == oChildNew.idGlobalRsrc:
+ iChildOld += 1;
+ else:
+ aoChanges.append(AttributeChangeEntry('Global Resource #%s' % (oChildNew.idGlobalRsrc,),
+ oChildNew, None,
+ '%s (#%u)' % (oChildNew.sName, oChildNew.idGlobalRsrc,),
+ 'Did not exist'));
+
+ # Done.
+ aoEntries.append(ChangeLogEntry(uidAuthor, None, tsEffective, tsExpire, oNew, oOld, aoChanges));
+
+ # If we're at the end of the log, add the initial entry.
+ if len(aoRows) <= cMaxRows and aoRows:
+ oNew = aoRows[-1];
+ aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None,
+ aaoChanges[-1][0], aaoChanges[-2][0] if len(aaoChanges) > 1 else oNew.tsExpire,
+ oNew, None, []));
+
+ return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
+
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Add a new testcase to the DB.
+ """
+
+ #
+ # Validate the input first.
+ #
+ assert isinstance(oData, TestCaseDataEx);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dErrors:
+ raise TMInvalidData('Invalid input data: %s' % (dErrors,));
+
+ #
+ # Add the testcase.
+ #
+ self._oDb.callProc('TestCaseLogic_addEntry',
+ ( uidAuthor, oData.sName, oData.sDescription, oData.fEnabled, oData.cSecTimeout,
+ oData.sTestBoxReqExpr, oData.sBuildReqExpr, oData.sBaseCmd, oData.sValidationKitZips,
+ oData.sComment ));
+ oData.idTestCase = self._oDb.fetchOne()[0];
+
+ # Add testcase dependencies.
+ for oDep in oData.aoDepTestCases:
+ self._oDb.execute('INSERT INTO TestCaseDeps (idTestCase, idTestCasePreReq, uidAuthor) VALUES (%s, %s, %s)'
+ , (oData.idTestCase, oDep.idTestCase, uidAuthor))
+
+ # Add global resource dependencies.
+ for oDep in oData.aoDepGlobalResources:
+ self._oDb.execute('INSERT INTO TestCaseGlobalRsrcDeps (idTestCase, idGlobalRsrc, uidAuthor) VALUES (%s, %s, %s)'
+ , (oData.idTestCase, oDep.idGlobalRsrc, uidAuthor))
+
+ # Set Test Case Arguments variations
+ for oVar in oData.aoTestCaseArgs:
+ self._oDb.execute('INSERT INTO TestCaseArgs (\n'
+ ' idTestCase, uidAuthor, sArgs, cSecTimeout,\n'
+ ' sTestBoxReqExpr, sBuildReqExpr, cGangMembers, sSubName)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)'
+ , ( oData.idTestCase, uidAuthor, oVar.sArgs, oVar.cSecTimeout,
+ oVar.sTestBoxReqExpr, oVar.sBuildReqExpr, oVar.cGangMembers, oVar.sSubName, ));
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def editEntry(self, oData, uidAuthor, fCommit = False): # pylint: disable=too-many-locals
+ """
+ Edit a testcase entry (extended).
+ Caller is expected to rollback the database transactions on exception.
+ """
+
+ #
+ # Validate the input.
+ #
+ assert isinstance(oData, TestCaseDataEx);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('Invalid input data: %s' % (dErrors,));
+
+ #
+ # Did anything change? If not return straight away.
+ #
+ oOldDataEx = TestCaseDataEx().initFromDbWithId(self._oDb, oData.idTestCase);
+ if oOldDataEx.isEqual(oData):
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ #
+ # Make the necessary changes.
+ #
+
+ # The test case itself.
+ if not TestCaseData().initFromOther(oOldDataEx).isEqual(oData):
+ self._oDb.callProc('TestCaseLogic_editEntry', ( uidAuthor, oData.idTestCase, oData.sName, oData.sDescription,
+ oData.fEnabled, oData.cSecTimeout, oData.sTestBoxReqExpr,
+ oData.sBuildReqExpr, oData.sBaseCmd, oData.sValidationKitZips,
+ oData.sComment ));
+ oData.idGenTestCase = self._oDb.fetchOne()[0];
+
+ #
+ # Its dependencies on other testcases.
+ #
+ aidNewDeps = [oDep.idTestCase for oDep in oData.aoDepTestCases];
+ aidOldDeps = [oDep.idTestCase for oDep in oOldDataEx.aoDepTestCases];
+
+ sQuery = self._oDb.formatBindArgs('UPDATE TestCaseDeps\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::timestamp\n'
+ , (oData.idTestCase,));
+ asKeepers = [];
+ for idDep in aidOldDeps:
+ if idDep in aidNewDeps:
+ asKeepers.append(str(idDep));
+ if asKeepers:
+ sQuery += ' AND idTestCasePreReq NOT IN (' + ', '.join(asKeepers) + ')\n';
+ self._oDb.execute(sQuery);
+
+ for idDep in aidNewDeps:
+ if idDep not in aidOldDeps:
+ self._oDb.execute('INSERT INTO TestCaseDeps (idTestCase, idTestCasePreReq, uidAuthor)\n'
+ 'VALUES (%s, %s, %s)\n'
+ , (oData.idTestCase, idDep, uidAuthor) );
+
+ #
+ # Its dependencies on global resources.
+ #
+ aidNewDeps = [oDep.idGlobalRsrc for oDep in oData.aoDepGlobalResources];
+ aidOldDeps = [oDep.idGlobalRsrc for oDep in oOldDataEx.aoDepGlobalResources];
+
+ sQuery = self._oDb.formatBindArgs('UPDATE TestCaseGlobalRsrcDeps\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::timestamp\n'
+ , (oData.idTestCase,));
+ asKeepers = [];
+ for idDep in aidOldDeps:
+ if idDep in aidNewDeps:
+ asKeepers.append(str(idDep));
+ if asKeepers:
+ sQuery = ' AND idGlobalRsrc NOT IN (' + ', '.join(asKeepers) + ')\n';
+ self._oDb.execute(sQuery);
+
+ for idDep in aidNewDeps:
+ if idDep not in aidOldDeps:
+ self._oDb.execute('INSERT INTO TestCaseGlobalRsrcDeps (idTestCase, idGlobalRsrc, uidAuthor)\n'
+ 'VALUES (%s, %s, %s)\n'
+ , (oData.idTestCase, idDep, uidAuthor) );
+
+ #
+ # Update Test Case Args
+ # Note! Primary key is idTestCase, tsExpire, sArgs.
+ #
+
+ # Historize rows that have been removed.
+ sQuery = self._oDb.formatBindArgs('UPDATE TestCaseArgs\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP'
+ , (oData.idTestCase, ));
+ for oNewVar in oData.aoTestCaseArgs:
+ asKeepers.append(self._oDb.formatBindArgs('%s', (oNewVar.sArgs,)));
+ if asKeepers:
+ sQuery += ' AND sArgs NOT IN (' + ', '.join(asKeepers) + ')\n';
+ self._oDb.execute(sQuery);
+
+ # Add new TestCaseArgs records if necessary, reusing old IDs when possible.
+ from testmanager.core.testcaseargs import TestCaseArgsData;
+ for oNewVar in oData.aoTestCaseArgs:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND sArgs = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (oData.idTestCase, oNewVar.sArgs,));
+ aoRow = self._oDb.fetchOne();
+ if aoRow is None:
+ # New
+ self._oDb.execute('INSERT INTO TestCaseArgs (\n'
+ ' idTestCase, uidAuthor, sArgs, cSecTimeout,\n'
+ ' sTestBoxReqExpr, sBuildReqExpr, cGangMembers, sSubName)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)'
+ , ( oData.idTestCase, uidAuthor, oNewVar.sArgs, oNewVar.cSecTimeout,
+ oNewVar.sTestBoxReqExpr, oNewVar.sBuildReqExpr, oNewVar.cGangMembers, oNewVar.sSubName));
+ else:
+ oCurVar = TestCaseArgsData().initFromDbRow(aoRow);
+ if self._oDb.isTsInfinity(oCurVar.tsExpire):
+ # Existing current entry, updated if changed.
+ if oNewVar.cSecTimeout == oCurVar.cSecTimeout \
+ and oNewVar.sTestBoxReqExpr == oCurVar.sTestBoxReqExpr \
+ and oNewVar.sBuildReqExpr == oCurVar.sBuildReqExpr \
+ and oNewVar.cGangMembers == oCurVar.cGangMembers \
+ and oNewVar.sSubName == oCurVar.sSubName:
+ oNewVar.idTestCaseArgs = oCurVar.idTestCaseArgs;
+ oNewVar.idGenTestCaseArgs = oCurVar.idGenTestCaseArgs;
+ continue; # Unchanged.
+ self._oDb.execute('UPDATE TestCaseArgs SET tsExpire = CURRENT_TIMESTAMP WHERE idGenTestCaseArgs = %s\n'
+ , (oCurVar.idGenTestCaseArgs, ));
+ else:
+ # Existing old entry, re-use the ID.
+ pass;
+ self._oDb.execute('INSERT INTO TestCaseArgs (\n'
+ ' idTestCaseArgs, idTestCase, uidAuthor, sArgs, cSecTimeout,\n'
+ ' sTestBoxReqExpr, sBuildReqExpr, cGangMembers, sSubName)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)\n'
+ 'RETURNING idGenTestCaseArgs\n'
+ , ( oCurVar.idTestCaseArgs, oData.idTestCase, uidAuthor, oNewVar.sArgs, oNewVar.cSecTimeout,
+ oNewVar.sTestBoxReqExpr, oNewVar.sBuildReqExpr, oNewVar.cGangMembers, oNewVar.sSubName));
+ oNewVar.idGenTestCaseArgs = self._oDb.fetchOne()[0];
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def removeEntry(self, uidAuthor, idTestCase, fCascade = False, fCommit = False):
+ """ Deletes the test case if possible. """
+ self._oDb.callProc('TestCaseLogic_delEntry', (uidAuthor, idTestCase, fCascade));
+ self._oDb.maybeCommit(fCommit);
+ return True
+
+
+ def getTestCasePreReqIds(self, idTestCase, tsEffective = None, cMax = None):
+ """
+ Returns an array of prerequisite testcases (IDs) for the given testcase.
+ May raise exception on database error or if the result exceeds cMax.
+ """
+ if tsEffective is None:
+ self._oDb.execute('SELECT idTestCasePreReq\n'
+ 'FROM TestCaseDeps\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY idTestCasePreReq\n'
+ , (idTestCase,) );
+ else:
+ self._oDb.execute('SELECT idTestCasePreReq\n'
+ 'FROM TestCaseDeps\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY idTestCasePreReq\n'
+ , (idTestCase, tsEffective, tsEffective) );
+
+
+ if cMax is not None and self._oDb.getRowCount() > cMax:
+ raise TMExceptionBase('Too many prerequisites for testcase %s: %s, max %s'
+ % (idTestCase, cMax, self._oDb.getRowCount(),));
+
+ aidPreReqs = [];
+ for aoRow in self._oDb.fetchAll():
+ aidPreReqs.append(aoRow[0]);
+ return aidPreReqs;
+
+
+ def cachedLookup(self, idTestCase):
+ """
+ Looks up the most recent TestCaseDataEx object for idTestCase
+ via an object cache.
+
+ Returns a shared TestCaseDataEx object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('TestCaseDataEx');
+ oEntry = self.dCache.get(idTestCase, None);
+ if oEntry is None:
+ fNeedTsNow = False;
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCases\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestCase, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCases\n'
+ 'WHERE idTestCase = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idTestCase, ));
+ fNeedTsNow = True;
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestCase));
+
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ oEntry = TestCaseDataEx();
+ tsNow = oEntry.initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None;
+ oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow);
+ self.dCache[idTestCase] = oEntry;
+ return oEntry;
+
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class TestCaseGlobalRsrcDepDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestCaseGlobalRsrcDepData(),];
+
+class TestCaseDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestCaseData(),];
+
+ def testEmptyExpr(self):
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr(None), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr(''), None);
+
+ def testSimpleExpr(self):
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('cMbMemory > 10'), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('cMbScratch < 10'), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('fChipsetIoMmu'), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('fChipsetIoMmu is True'), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('fChipsetIoMmu is False'), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('fChipsetIoMmu is None'), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('isinstance(fChipsetIoMmu, bool)'), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('isinstance(iTestBoxScriptRev, int)'), None);
+ self.assertEqual(TestCaseData.validateTestBoxReqExpr('isinstance(cMbScratch, long)'), None);
+
+ def testBadExpr(self):
+ self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('this is an bad expression, surely it must be'), None);
+ self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('x = 1 + 1'), None);
+ self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('__import__(\'os\').unlink(\'/tmp/no/such/file\')'), None);
+ self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('print "foobar"'), None);
+
+class TestCaseDataExTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestCaseDataEx(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testcaseargs.py b/src/VBox/ValidationKit/testmanager/core/testcaseargs.py
new file mode 100755
index 00000000..e63df090
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testcaseargs.py
@@ -0,0 +1,416 @@
+# -*- coding: utf-8 -*-
+# $Id: testcaseargs.py $
+
+"""
+Test Manager - Test Case Arguments Variations.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+import sys;
+
+# Validation Kit imports.
+from common import utils;
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \
+ TMRowNotFound;
+from testmanager.core.testcase import TestCaseData, TestCaseDependencyLogic, TestCaseGlobalRsrcDepLogic;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ long = int; # pylint: disable=redefined-builtin,invalid-name
+
+
+class TestCaseArgsData(ModelDataBase):
+ """
+ Test case argument variation.
+ """
+
+ ksIdAttr = 'idTestCaseArgs';
+ ksIdGenAttr = 'idGenTestCaseArgs';
+
+ ksParam_idTestCase = 'TestCaseArgs_idTestCase';
+ ksParam_idTestCaseArgs = 'TestCaseArgs_idTestCaseArgs';
+ ksParam_tsEffective = 'TestCaseArgs_tsEffective';
+ ksParam_tsExpire = 'TestCaseArgs_tsExpire';
+ ksParam_uidAuthor = 'TestCaseArgs_uidAuthor';
+ ksParam_idGenTestCaseArgs = 'TestCaseArgs_idGenTestCaseArgs';
+ ksParam_sArgs = 'TestCaseArgs_sArgs';
+ ksParam_cSecTimeout = 'TestCaseArgs_cSecTimeout';
+ ksParam_sTestBoxReqExpr = 'TestCaseArgs_sTestBoxReqExpr';
+ ksParam_sBuildReqExpr = 'TestCaseArgs_sBuildReqExpr';
+ ksParam_cGangMembers = 'TestCaseArgs_cGangMembers';
+ ksParam_sSubName = 'TestCaseArgs_sSubName';
+
+ kcDbColumns = 12;
+
+ kasAllowNullAttributes = [ 'idTestCase', 'idTestCaseArgs', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestCaseArgs',
+ 'cSecTimeout', 'sTestBoxReqExpr', 'sBuildReqExpr', 'sSubName', ];
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestCase = None;
+ self.idTestCaseArgs = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.idGenTestCaseArgs = None;
+ self.sArgs = '';
+ self.cSecTimeout = None;
+ self.sTestBoxReqExpr = None;
+ self.sBuildReqExpr = None;
+ self.cGangMembers = 1;
+ self.sSubName = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the object from a SELECT * FROM TestCaseArgs row.
+ Returns self. Raises exception if aoRow is None.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('TestBoxStatus not found.');
+
+ self.idTestCase = aoRow[0];
+ self.idTestCaseArgs = aoRow[1];
+ self.tsEffective = aoRow[2];
+ self.tsExpire = aoRow[3];
+ self.uidAuthor = aoRow[4];
+ self.idGenTestCaseArgs = aoRow[5];
+ self.sArgs = aoRow[6];
+ self.cSecTimeout = aoRow[7];
+ self.sTestBoxReqExpr = aoRow[8];
+ self.sBuildReqExpr = aoRow[9];
+ self.cGangMembers = aoRow[10];
+ self.sSubName = aoRow[11];
+ return self;
+
+ def initFromDbWithId(self, oDb, idTestCaseArgs, tsNow = None, sPeriodBack = None):
+ """
+ Initialize from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE idTestCaseArgs = %s\n'
+ , ( idTestCaseArgs,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idTestCaseArgs=%s not found (tsNow=%s sPeriodBack=%s)'
+ % (idTestCaseArgs, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+ def initFromDbWithGenId(self, oDb, idGenTestCaseArgs):
+ """
+ Initialize from the database, given the generation ID of a row.
+ """
+ oDb.execute('SELECT * FROM TestCaseArgs WHERE idGenTestCaseArgs = %s', (idGenTestCaseArgs,));
+ return self.initFromDbRow(oDb.fetchOne());
+
+ def initFromValues(self, sArgs, cSecTimeout = None, sTestBoxReqExpr = None, sBuildReqExpr = None, # pylint: disable=too-many-arguments
+ cGangMembers = 1, idTestCase = None, idTestCaseArgs = None, tsEffective = None, tsExpire = None,
+ uidAuthor = None, idGenTestCaseArgs = None, sSubName = None):
+ """
+ Reinitialize from values.
+ Returns self.
+ """
+ self.idTestCase = idTestCase;
+ self.idTestCaseArgs = idTestCaseArgs;
+ self.tsEffective = tsEffective;
+ self.tsExpire = tsExpire;
+ self.uidAuthor = uidAuthor;
+ self.idGenTestCaseArgs = idGenTestCaseArgs;
+ self.sArgs = sArgs;
+ self.cSecTimeout = utils.parseIntervalSeconds(cSecTimeout);
+ self.sTestBoxReqExpr = sTestBoxReqExpr;
+ self.sBuildReqExpr = sBuildReqExpr;
+ self.cGangMembers = cGangMembers;
+ self.sSubName = sSubName;
+ return self;
+
+ def getAttributeParamNullValues(self, sAttr):
+ aoNilValues = ModelDataBase.getAttributeParamNullValues(self, sAttr);
+ if sAttr == 'cSecTimeout':
+ aoNilValues.insert(0, ''); # Prettier NULL value for cSecTimeout.
+ elif sAttr == 'sArgs':
+ aoNilValues = []; # No NULL value here, thank you.
+ return aoNilValues;
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ if sAttr == 'cSecTimeout' and oValue not in aoNilValues: # Allow human readable interval formats.
+ return utils.parseIntervalSeconds(oValue);
+
+ (oValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+ if sError is None:
+ if sAttr == 'sTestBoxReqExpr':
+ sError = TestCaseData.validateTestBoxReqExpr(oValue);
+ elif sAttr == 'sBuildReqExpr':
+ sError = TestCaseData.validateBuildReqExpr(oValue);
+ return (oValue, sError);
+
+
+
+
+class TestCaseArgsDataEx(TestCaseArgsData):
+ """
+ Complete data set.
+ """
+
+ def __init__(self):
+ TestCaseArgsData.__init__(self);
+ self.oTestCase = None;
+ self.aoTestCasePreReqs = [];
+ self.aoGlobalRsrc = [];
+
+ def initFromDbRow(self, aoRow):
+ raise TMExceptionBase('Do not call me: %s' % (aoRow,))
+
+ def initFromDbRowEx(self, aoRow, oDb, tsConfigEff = None, tsRsrcEff = None):
+ """
+ Extended version of initFromDbRow that fills in the rest from the database.
+ """
+ TestCaseArgsData.initFromDbRow(self, aoRow);
+
+ if tsConfigEff is None: tsConfigEff = oDb.getCurrentTimestamp();
+ if tsRsrcEff is None: tsRsrcEff = oDb.getCurrentTimestamp();
+
+ self.oTestCase = TestCaseData().initFromDbWithId(oDb, self.idTestCase, tsConfigEff);
+ self.aoTestCasePreReqs = TestCaseDependencyLogic(oDb).getTestCaseDeps(self.idTestCase, tsConfigEff);
+ self.aoGlobalRsrc = TestCaseGlobalRsrcDepLogic(oDb).getTestCaseDeps(self.idTestCase, tsRsrcEff);
+
+ return self;
+
+ def initFromDbWithId(self, oDb, idTestCaseArgs, tsNow = None, sPeriodBack = None):
+ _ = oDb; _ = idTestCaseArgs; _ = tsNow; _ = sPeriodBack;
+ raise TMExceptionBase('Not supported.');
+
+ def initFromDbWithGenId(self, oDb, idGenTestCaseArgs):
+ _ = oDb; _ = idGenTestCaseArgs;
+ raise TMExceptionBase('Use initFromDbWithGenIdEx...');
+
+ def initFromDbWithGenIdEx(self, oDb, idGenTestCaseArgs, tsConfigEff = None, tsRsrcEff = None):
+ """
+ Initialize from the database, given the ID of a row.
+ """
+ oDb.execute('SELECT *, CURRENT_TIMESTAMP FROM TestCaseArgs WHERE idGenTestCaseArgs = %s', (idGenTestCaseArgs,));
+ aoRow = oDb.fetchOne();
+ return self.initFromDbRowEx(aoRow, oDb, tsConfigEff, tsRsrcEff);
+
+ def convertFromParamNull(self):
+ raise TMExceptionBase('Not implemented');
+
+ def convertToParamNull(self):
+ raise TMExceptionBase('Not implemented');
+
+ def isEqual(self, oOther):
+ raise TMExceptionBase('Not implemented');
+
+ def matchesTestBoxProps(self, oTestBoxData):
+ """
+ Checks if the all of the testbox related test requirements matches the
+ given testbox.
+
+ Returns True or False according to the expression, None on exception or
+ non-boolean expression result.
+ """
+ return TestCaseData.matchesTestBoxPropsEx(oTestBoxData, self.oTestCase.sTestBoxReqExpr) \
+ and TestCaseData.matchesTestBoxPropsEx(oTestBoxData, self.sTestBoxReqExpr);
+
+ def matchesBuildProps(self, oBuildDataEx):
+ """
+ Checks if the all of the build related test requirements matches the
+ given build.
+
+ Returns True or False according to the expression, None on exception or
+ non-boolean expression result.
+ """
+ return TestCaseData.matchesBuildPropsEx(oBuildDataEx, self.oTestCase.sBuildReqExpr) \
+ and TestCaseData.matchesBuildPropsEx(oBuildDataEx, self.sBuildReqExpr);
+
+
+class TestCaseArgsLogic(ModelLogicBase):
+ """
+ TestCaseArgs database logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb);
+ self.dCache = None;
+
+
+ def areResourcesFree(self, oDataEx):
+ """
+ Checks if all global resources are currently still in existance and free.
+ Returns True/False. May raise exception on database error.
+ """
+
+ # Create a set of global resource IDs.
+ if not oDataEx.aoGlobalRsrc:
+ return True;
+ asIdRsrcs = [str(oDep.idGlobalRsrc) for oDep, _ in oDataEx.aoGlobalRsrc];
+
+ # A record in the resource status table means it's allocated.
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM GlobalResourceStatuses\n'
+ 'WHERE GlobalResourceStatuses.idGlobalRsrc IN (' + ', '.join(asIdRsrcs) + ')\n');
+ if self._oDb.fetchOne()[0] == 0:
+ # Check for disabled or deleted resources (we cannot allocate them).
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM GlobalResources\n'
+ 'WHERE GlobalResources.idGlobalRsrc IN (' + ', '.join(asIdRsrcs) + ')\n'
+ ' AND GlobalResources.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND GlobalResources.fEnabled = TRUE\n');
+ if self._oDb.fetchOne()[0] == len(oDataEx.aoGlobalRsrc):
+ return True;
+ return False;
+
+ def getAll(self):
+ """Get list of objects of type TestCaseArgsData"""
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP')
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(TestCaseArgsData().initFromDbRow(aoRow))
+
+ return aoRet
+
+ def getTestCaseArgs(self, idTestCase, tsNow = None, aiWhiteList = None):
+ """Get list of testcase's arguments variations"""
+ if aiWhiteList is None:
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY TestCaseArgs.idTestCaseArgs\n'
+ , (idTestCase,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY TestCaseArgs.idTestCaseArgs\n'
+ , (idTestCase, tsNow, tsNow));
+ else:
+ sWhiteList = ','.join((str(x) for x in aiWhiteList));
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND idTestCaseArgs IN (' + sWhiteList + ')\n'
+ 'ORDER BY TestCaseArgs.idTestCaseArgs\n'
+ , (idTestCase,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE idTestCase = %s\n'
+ ' AND tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ ' AND idTestCaseArgs IN (' + sWhiteList + ')\n'
+ 'ORDER BY TestCaseArgs.idTestCaseArgs\n'
+ , (idTestCase, tsNow, tsNow));
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(TestCaseArgsData().initFromDbRow(aoRow))
+
+ return aoRet
+
+ def addTestCaseArgs(self, oTestCaseArgsData):
+ """Add Test Case Args record into DB"""
+ pass; # pylint: disable=unnecessary-pass
+
+ def cachedLookup(self, idTestCaseArgs):
+ """
+ Looks up the most recent TestCaseArgsDataEx object for idTestCaseArg
+ via in an object cache.
+
+ Returns a shared TestCaseArgDataEx object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('TestCaseArgsDataEx');
+ oEntry = self.dCache.get(idTestCaseArgs, None);
+ if oEntry is None:
+ fNeedTsNow = False;
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE idTestCaseArgs = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestCaseArgs, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestCaseArgs\n'
+ 'WHERE idTestCaseArgs = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idTestCaseArgs, ));
+ fNeedTsNow = True;
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestCaseArgs));
+
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ oEntry = TestCaseArgsDataEx();
+ tsNow = TestCaseArgsData().initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None;
+ oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow, tsNow);
+ self.dCache[idTestCaseArgs] = oEntry;
+ return oEntry;
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class TestCaseArgsDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestCaseArgsData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testgroup.py b/src/VBox/ValidationKit/testmanager/core/testgroup.py
new file mode 100755
index 00000000..9a648b0d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testgroup.py
@@ -0,0 +1,771 @@
+# -*- coding: utf-8 -*-
+# $Id: testgroup.py $
+
+"""
+Test Manager - Test groups management.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMRowInUse, \
+ TMTooManyRows, TMInvalidData, TMRowNotFound, TMRowAlreadyExists;
+from testmanager.core.testcase import TestCaseData, TestCaseDataEx;
+
+
+class TestGroupMemberData(ModelDataBase):
+ """Representation of a test group member database row."""
+
+ ksParam_idTestGroup = 'TestGroupMember_idTestGroup';
+ ksParam_idTestCase = 'TestGroupMember_idTestCase';
+ ksParam_tsEffective = 'TestGroupMember_tsEffective';
+ ksParam_tsExpire = 'TestGroupMember_tsExpire';
+ ksParam_uidAuthor = 'TestGroupMember_uidAuthor';
+ ksParam_iSchedPriority = 'TestGroupMember_iSchedPriority';
+ ksParam_aidTestCaseArgs = 'TestGroupMember_aidTestCaseArgs';
+
+ kasAllowNullAttributes = ['idTestGroup', 'idTestCase', 'tsEffective', 'tsExpire', 'uidAuthor', 'aidTestCaseArgs' ];
+ kiMin_iSchedPriority = 0;
+ kiMax_iSchedPriority = 31;
+
+ kcDbColumns = 7;
+
+ def __init__(self):
+ ModelDataBase.__init__(self)
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestGroup = None;
+ self.idTestCase = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.iSchedPriority = 16;
+ self.aidTestCaseArgs = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestCaseGroupMembers.
+ Return self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test group member not found.')
+
+ self.idTestGroup = aoRow[0];
+ self.idTestCase = aoRow[1];
+ self.tsEffective = aoRow[2];
+ self.tsExpire = aoRow[3];
+ self.uidAuthor = aoRow[4];
+ self.iSchedPriority = aoRow[5];
+ self.aidTestCaseArgs = aoRow[6];
+ return self
+
+
+ def getAttributeParamNullValues(self, sAttr):
+ # Arrays default to [] as NULL currently. That doesn't work for us.
+ if sAttr == 'aidTestCaseArgs':
+ aoNilValues = [None, '-1'];
+ else:
+ aoNilValues = ModelDataBase.getAttributeParamNullValues(self, sAttr);
+ return aoNilValues;
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ if sAttr != 'aidTestCaseArgs':
+ return ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+
+ # -1 is a special value, which when present make the whole thing NULL (None).
+ (aidVariations, sError) = self.validateListOfInts(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
+ iMin = -1, iMax = 0x7ffffffe);
+ if sError is None:
+ if aidVariations is None:
+ pass;
+ elif -1 in aidVariations:
+ aidVariations = None;
+ elif 0 in aidVariations:
+ sError = 'Invalid test case varation ID #0.';
+ else:
+ aidVariations = sorted(aidVariations);
+ return (aidVariations, sError);
+
+
+
+class TestGroupMemberDataEx(TestGroupMemberData):
+ """Extended representation of a test group member."""
+
+ def __init__(self):
+ """Extend parent class"""
+ TestGroupMemberData.__init__(self)
+ self.oTestCase = None; # TestCaseDataEx.
+
+ def initFromDbRowEx(self, aoRow, oDb, tsNow = None):
+ """
+ Reinitialize from a SELECT * FROM TestGroupMembers, TestCases row.
+ Will query the necessary additional data from oDb using tsNow.
+
+ Returns self. Raises exception if no row or database error.
+ """
+ TestGroupMemberData.initFromDbRow(self, aoRow);
+ self.oTestCase = TestCaseDataEx();
+ self.oTestCase.initFromDbRowEx(aoRow[TestGroupMemberData.kcDbColumns:], oDb, tsNow);
+ return self;
+
+ def initFromParams(self, oDisp, fStrict = True):
+ self.oTestCase = None;
+ return TestGroupMemberData.initFromParams(self, oDisp, fStrict);
+
+ def getDataAttributes(self):
+ asAttributes = TestGroupMemberData.getDataAttributes(self);
+ asAttributes.remove('oTestCase');
+ return asAttributes;
+
+ def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other):
+ dErrors = TestGroupMemberData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor);
+ if self.ksParam_idTestCase not in dErrors:
+ self.oTestCase = TestCaseDataEx()
+ try:
+ self.oTestCase.initFromDbWithId(oDb, self.idTestCase);
+ except Exception as oXcpt:
+ self.oTestCase = TestCaseDataEx()
+ dErrors[self.ksParam_idTestCase] = str(oXcpt);
+ return dErrors;
+
+
+class TestGroupMemberData2(TestCaseData):
+ """Special representation of a Test Group Member item"""
+
+ def __init__(self):
+ """Extend parent class"""
+ TestCaseData.__init__(self)
+ self.idTestGroup = None
+ self.aidTestCaseArgs = []
+
+ def initFromDbRowEx(self, aoRow):
+ """
+ Reinitialize from this query:
+
+ SELECT TestCases.*,
+ TestGroupMembers.idTestGroup,
+ TestGroupMembers.aidTestCaseArgs
+ FROM TestCases, TestGroupMembers
+ WHERE TestCases.idTestCase = TestGroupMembers.idTestCase
+
+ Represents complete test group member (test case) info.
+ Returns object of type TestGroupMemberData2. Raises exception if no row.
+ """
+ TestCaseData.initFromDbRow(self, aoRow);
+ self.idTestGroup = aoRow[-2]
+ self.aidTestCaseArgs = aoRow[-1]
+ return self;
+
+
+class TestGroupData(ModelDataBase):
+ """
+ Test group data.
+ """
+
+ ksIdAttr = 'idTestGroup';
+
+ ksParam_idTestGroup = 'TestGroup_idTestGroup'
+ ksParam_tsEffective = 'TestGroup_tsEffective'
+ ksParam_tsExpire = 'TestGroup_tsExpire'
+ ksParam_uidAuthor = 'TestGroup_uidAuthor'
+ ksParam_sName = 'TestGroup_sName'
+ ksParam_sDescription = 'TestGroup_sDescription'
+ ksParam_sComment = 'TestGroup_sComment'
+
+ kasAllowNullAttributes = ['idTestGroup', 'tsEffective', 'tsExpire', 'uidAuthor', 'sDescription', 'sComment' ];
+
+ kcDbColumns = 7;
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestGroup = None
+ self.tsEffective = None
+ self.tsExpire = None
+ self.uidAuthor = None
+ self.sName = None
+ self.sDescription = None
+ self.sComment = None
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestGroups row.
+ Returns object of type TestGroupData. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test group not found.')
+
+ self.idTestGroup = aoRow[0]
+ self.tsEffective = aoRow[1]
+ self.tsExpire = aoRow[2]
+ self.uidAuthor = aoRow[3]
+ self.sName = aoRow[4]
+ self.sDescription = aoRow[5]
+ self.sComment = aoRow[6]
+ return self
+
+ def initFromDbWithId(self, oDb, idTestGroup, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE idTestGroup = %s\n'
+ , ( idTestGroup,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idTestGroup=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestGroup, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+
+class TestGroupDataEx(TestGroupData):
+ """
+ Extended test group data.
+ """
+
+ ksParam_aoMembers = 'TestGroupDataEx_aoMembers';
+ kasAltArrayNull = [ 'aoMembers', ];
+
+ ## Helper parameter containing the comma separated list with the IDs of
+ # potential members found in the parameters.
+ ksParam_aidTestCases = 'TestGroupDataEx_aidTestCases';
+
+
+ def __init__(self):
+ TestGroupData.__init__(self);
+ self.aoMembers = []; # TestGroupMemberDataEx.
+
+ def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None):
+ """
+ Worker shared by the initFromDb* methods.
+ Returns self. Raises exception if no row or database error.
+ """
+ self.aoMembers = [];
+ _ = sPeriodBack; ## @todo sPeriodBack
+
+ if tsNow is None:
+ oDb.execute('SELECT TestGroupMembers.*, TestCases.*\n'
+ 'FROM TestGroupMembers\n'
+ 'LEFT OUTER JOIN TestCases ON (\n'
+ ' TestGroupMembers.idTestCase = TestCases.idTestCase\n'
+ ' AND TestCases.tsExpire = \'infinity\'::TIMESTAMP)\n'
+ 'WHERE TestGroupMembers.idTestGroup = %s\n'
+ ' AND TestGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY TestCases.sName, TestCases.idTestCase\n'
+ , (self.idTestGroup,));
+ else:
+ oDb.execute('SELECT TestGroupMembers.*, TestCases.*\n'
+ 'FROM TestGroupMembers\n'
+ 'LEFT OUTER JOIN TestCases ON (\n'
+ ' TestGroupMembers.idTestCase = TestCases.idTestCase\n'
+ ' AND TestCases.tsExpire > %s\n'
+ ' AND TestCases.tsEffective <= %s)\n'
+ 'WHERE TestGroupMembers.idTestGroup = %s\n'
+ ' AND TestGroupMembers.tsExpire > %s\n'
+ ' AND TestGroupMembers.tsEffective <= %s\n'
+ 'ORDER BY TestCases.sName, TestCases.idTestCase\n'
+ , (tsNow, tsNow, self.idTestGroup, tsNow, tsNow));
+
+ for aoRow in oDb.fetchAll():
+ self.aoMembers.append(TestGroupMemberDataEx().initFromDbRowEx(aoRow, oDb, tsNow));
+ return self;
+
+ def initFromDbRowEx(self, aoRow, oDb, tsNow = None, sPeriodBack = None):
+ """
+ Reinitialize from a SELECT * FROM TestGroups row. Will query the
+ necessary additional data from oDb using tsNow.
+ Returns self. Raises exception if no row or database error.
+ """
+ TestGroupData.initFromDbRow(self, aoRow);
+ return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack);
+
+ def initFromDbWithId(self, oDb, idTestGroup, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ TestGroupData.initFromDbWithId(self, oDb, idTestGroup, tsNow, sPeriodBack);
+ return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack);
+
+
+ def getAttributeParamNullValues(self, sAttr):
+ if sAttr != 'aoMembers':
+ return TestGroupData.getAttributeParamNullValues(self, sAttr);
+ return ['', [], None];
+
+ def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
+ if sAttr != 'aoMembers':
+ return TestGroupData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict);
+
+ aoNewValue = [];
+ aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = [])
+ sIds = oDisp.getStringParam(self.ksParam_aidTestCases, sDefault = '');
+ for idTestCase in sIds.split(','):
+ try: idTestCase = int(idTestCase);
+ except: pass;
+ oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestGroupDataEx.ksParam_aoMembers, idTestCase,))
+ oMember = TestGroupMemberDataEx().initFromParams(oDispWrapper, fStrict = False);
+ if idTestCase in aidSelected:
+ aoNewValue.append(oMember);
+ return aoNewValue;
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ if sAttr != 'aoMembers':
+ return TestGroupData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+
+ asErrors = [];
+ aoNewMembers = [];
+ for oOldMember in oValue:
+ oNewMember = TestGroupMemberDataEx().initFromOther(oOldMember);
+ aoNewMembers.append(oNewMember);
+
+ dErrors = oNewMember.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other);
+ if dErrors:
+ asErrors.append(str(dErrors));
+
+ if not asErrors:
+ for i, _ in enumerate(aoNewMembers):
+ idTestCase = aoNewMembers[i];
+ for j in range(i + 1, len(aoNewMembers)):
+ if aoNewMembers[j].idTestCase == idTestCase:
+ asErrors.append('Duplicate testcase #%d!' % (idTestCase, ));
+ break;
+
+ return (aoNewMembers, None if not asErrors else '<br>\n'.join(asErrors));
+
+
+class TestGroupLogic(ModelLogicBase):
+ """
+ Test case management logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ #
+ # Standard methods.
+ #
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches test groups.
+
+ Returns an array (list) of TestGroupDataEx items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sName ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY sName ASC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(TestGroupDataEx().initFromDbRowEx(aoRow, self._oDb, tsNow));
+ return aoRet;
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Adds a testgroup to the database.
+ """
+
+ #
+ # Validate inputs.
+ #
+ assert isinstance(oData, TestGroupDataEx);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dErrors:
+ raise TMInvalidData('addEntry invalid input: %s' % (dErrors,));
+ self._assertUniq(oData, None);
+
+ #
+ # Do the job.
+ #
+ self._oDb.execute('INSERT INTO TestGroups (uidAuthor, sName, sDescription, sComment)\n'
+ 'VALUES (%s, %s, %s, %s)\n'
+ 'RETURNING idTestGroup\n'
+ , ( uidAuthor,
+ oData.sName,
+ oData.sDescription,
+ oData.sComment ));
+ idTestGroup = self._oDb.fetchOne()[0];
+ oData.idTestGroup = idTestGroup;
+
+ for oMember in oData.aoMembers:
+ oMember.idTestGroup = idTestGroup;
+ self._insertTestGroupMember(uidAuthor, oMember)
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Modifies a test group.
+ """
+
+ #
+ # Validate inputs and read in the old(/current) data.
+ #
+ assert isinstance(oData, TestGroupDataEx);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
+ self._assertUniq(oData, oData.idTestGroup);
+
+ oOldData = TestGroupDataEx().initFromDbWithId(self._oDb, oData.idTestGroup);
+
+ #
+ # Update the data that needs updating.
+ #
+
+ if not oData.isEqualEx(oOldData, [ 'aoMembers', 'tsEffective', 'tsExpire', 'uidAuthor', ]):
+ self._historizeTestGroup(oData.idTestGroup);
+ self._oDb.execute('INSERT INTO TestGroups\n'
+ ' (uidAuthor, idTestGroup, sName, sDescription, sComment)\n'
+ 'VALUES (%s, %s, %s, %s, %s)\n'
+ , ( uidAuthor,
+ oData.idTestGroup,
+ oData.sName,
+ oData.sDescription,
+ oData.sComment ));
+
+ # Create a lookup dictionary for old entries.
+ dOld = {};
+ for oOld in oOldData.aoMembers:
+ dOld[oOld.idTestCase] = oOld;
+ assert len(dOld) == len(oOldData.aoMembers);
+
+ # Add new members, updated existing ones.
+ dNew = {};
+ for oNewMember in oData.aoMembers:
+ oNewMember.idTestGroup = oData.idTestGroup;
+ if oNewMember.idTestCase in dNew:
+ raise TMRowAlreadyExists('Duplicate test group member: idTestCase=%d (%s / %s)'
+ % (oNewMember.idTestCase, oNewMember, dNew[oNewMember.idTestCase],));
+ dNew[oNewMember.idTestCase] = oNewMember;
+
+ oOldMember = dOld.get(oNewMember.idTestCase, None);
+ if oOldMember is not None:
+ if oNewMember.isEqualEx(oOldMember, [ 'uidAuthor', 'tsEffective', 'tsExpire' ]):
+ continue; # Skip, nothing changed.
+ self._historizeTestGroupMember(oData.idTestGroup, oNewMember.idTestCase);
+ self._insertTestGroupMember(uidAuthor, oNewMember);
+
+ # Expire members that have been removed.
+ sQuery = self._oDb.formatBindArgs('UPDATE TestGroupMembers\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( oData.idTestGroup, ));
+ if dNew:
+ sQuery += ' AND idTestCase NOT IN (%s)' % (', '.join([str(iKey) for iKey in dNew]),);
+ self._oDb.execute(sQuery);
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def removeEntry(self, uidAuthor, idTestGroup, fCascade = False, fCommit = False):
+ """
+ Deletes a test group.
+ """
+ _ = uidAuthor; ## @todo record uidAuthor.
+
+ #
+ # Cascade.
+ #
+ if fCascade is not True:
+ self._oDb.execute('SELECT SchedGroups.idSchedGroup, SchedGroups.sName\n'
+ 'FROM SchedGroupMembers, SchedGroups\n'
+ 'WHERE SchedGroupMembers.idTestGroup = %s\n'
+ ' AND SchedGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND SchedGroups.idSchedGroup = SchedGroupMembers.idSchedGroup\n'
+ ' AND SchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( idTestGroup, ));
+ aoGroups = self._oDb.fetchAll();
+ if aoGroups:
+ asGroups = ['%s (#%d)' % (sName, idSchedGroup) for idSchedGroup, sName in aoGroups];
+ raise TMRowInUse('Test group #%d is member of one or more scheduling groups: %s'
+ % (idTestGroup, ', '.join(asGroups),));
+ else:
+ self._oDb.execute('UPDATE SchedGroupMembers\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( idTestGroup, ));
+
+ #
+ # Remove the group.
+ #
+ self._oDb.execute('UPDATE TestGroupMembers\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestGroup,))
+ self._oDb.execute('UPDATE TestGroups\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestGroup,))
+
+ self._oDb.maybeCommit(fCommit)
+ return True;
+
+ def cachedLookup(self, idTestGroup):
+ """
+ Looks up the most recent TestGroupDataEx object for idTestGroup
+ via an object cache.
+
+ Returns a shared TestGroupDataEx object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('TestGroupDataEx');
+ oEntry = self.dCache.get(idTestGroup, None);
+ if oEntry is None:
+ fNeedTsNow = False;
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE idTestGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestGroup, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE idTestGroup = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (idTestGroup, ));
+ fNeedTsNow = True;
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestGroup));
+
+ if self._oDb.getRowCount() == 1:
+ aaoRow = self._oDb.fetchOne();
+ oEntry = TestGroupDataEx();
+ tsNow = oEntry.initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None;
+ oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow);
+ self.dCache[idTestGroup] = oEntry;
+ return oEntry;
+
+
+ #
+ # Other methods.
+ #
+
+ def fetchOrderedByName(self, tsNow = None):
+ """
+ Return list of objects of type TestGroupData ordered by name.
+ May raise exception on database error.
+ """
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sName ASC\n');
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY sName ASC\n'
+ , (tsNow, tsNow,));
+ aoRet = []
+ for _ in range(self._oDb.getRowCount()):
+ aoRet.append(TestGroupData().initFromDbRow(self._oDb.fetchOne()));
+ return aoRet;
+
+ def getMembers(self, idTestGroup):
+ """
+ Fetches all test case records from DB which are
+ belong to current Test Group.
+ Returns list of objects of type TestGroupMemberData2 (!).
+ """
+ self._oDb.execute('SELECT TestCases.*,\n'
+ ' TestGroupMembers.idTestGroup,\n'
+ ' TestGroupMembers.aidTestCaseArgs\n'
+ 'FROM TestCases, TestGroupMembers\n'
+ 'WHERE TestCases.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestGroupMembers.idTestCase = TestCases.idTestCase\n'
+ ' AND TestGroupMembers.idTestGroup = %s\n'
+ 'ORDER BY TestCases.idTestCase ASC;',
+ (idTestGroup,))
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(TestGroupMemberData2().initFromDbRowEx(aoRow))
+
+ return aoRet
+
+ def getAll(self, tsNow=None):
+ """Return list of objects of type TestGroupData"""
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY idTestGroup ASC;')
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY idTestGroup ASC;',
+ (tsNow, tsNow))
+
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(TestGroupData().initFromDbRow(aoRow))
+
+ return aoRet
+
+ def getById(self, idTestGroup, tsNow=None):
+ """Get Test Group data by its ID"""
+
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE tsExpire = \'infinity\'::timestamp\n'
+ ' AND idTestGroup = %s\n'
+ 'ORDER BY idTestGroup ASC;'
+ , (idTestGroup,))
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestGroups\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ ' AND idTestGroup = %s\n'
+ 'ORDER BY idTestGroup ASC;'
+ , (tsNow, tsNow, idTestGroup))
+
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise TMTooManyRows('Found more than one test groups with the same credentials. Database structure is corrupted.')
+ try:
+ return TestGroupData().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+ #
+ # Helpers.
+ #
+
+ def _assertUniq(self, oData, idTestGroupIgnore):
+ """ Checks that the test group name is unique, raises exception if it isn't. """
+ self._oDb.execute('SELECT idTestGroup\n'
+ 'FROM TestGroups\n'
+ 'WHERE sName = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ + ('' if idTestGroupIgnore is None else ' AND idTestGroup <> %d\n' % (idTestGroupIgnore,))
+ , ( oData.sName, ))
+ if self._oDb.getRowCount() > 0:
+ raise TMRowAlreadyExists('A Test group with name "%s" already exist.' % (oData.sName,));
+ return True;
+
+ def _historizeTestGroup(self, idTestGroup):
+ """ Historize Test Group record. """
+ self._oDb.execute('UPDATE TestGroups\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestGroup = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( idTestGroup, ));
+ return True;
+
+ def _historizeTestGroupMember(self, idTestGroup, idTestCase):
+ """ Historize Test Group Member record. """
+ self._oDb.execute('UPDATE TestGroupMembers\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestGroup = %s\n'
+ ' AND idTestCase = %s\n'
+ ' AND tsExpire = \'infinity\'::timestamp\n'
+ , (idTestGroup, idTestCase,));
+ return True;
+
+ def _insertTestGroupMember(self, uidAuthor, oMember):
+ """ Inserts a test group member. """
+ self._oDb.execute('INSERT INTO TestGroupMembers\n'
+ ' (uidAuthor, idTestGroup, idTestCase, iSchedPriority, aidTestCaseArgs)\n'
+ 'VALUES (%s, %s, %s, %s, %s)\n'
+ , ( uidAuthor,
+ oMember.idTestGroup,
+ oMember.idTestCase,
+ oMember.iSchedPriority,
+ oMember.aidTestCaseArgs, ));
+ return True;
+
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class TestGroupMemberDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestGroupMemberData(),];
+
+class TestGroupDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestGroupData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testresultfailures.py b/src/VBox/ValidationKit/testmanager/core/testresultfailures.py
new file mode 100755
index 00000000..b94ff7d5
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testresultfailures.py
@@ -0,0 +1,529 @@
+# -*- coding: utf-8 -*-
+# $Id: testresultfailures.py $
+# pylint: disable=too-many-lines
+
+## @todo Rename this file to testresult.py!
+
+"""
+Test Manager - Test result failures.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports.
+import sys;
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMInvalidData, TMRowNotFound, \
+ TMRowAlreadyExists, ChangeLogEntry, AttributeChangeEntry;
+from testmanager.core.failurereason import FailureReasonData;
+from testmanager.core.useraccount import UserAccountLogic;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ xrange = range; # pylint: disable=redefined-builtin,invalid-name
+
+
+class TestResultFailureData(ModelDataBase):
+ """
+ Test result failure reason data.
+ """
+
+ ksIdAttr = 'idTestResult';
+ kfIdAttrIsForForeign = True; # Modifies the 'add' validation.
+
+ ksParam_idTestResult = 'TestResultFailure_idTestResult';
+ ksParam_tsEffective = 'TestResultFailure_tsEffective';
+ ksParam_tsExpire = 'TestResultFailure_tsExpire';
+ ksParam_uidAuthor = 'TestResultFailure_uidAuthor';
+ ksParam_idTestSet = 'TestResultFailure_idTestSet';
+ ksParam_idFailureReason = 'TestResultFailure_idFailureReason';
+ ksParam_sComment = 'TestResultFailure_sComment';
+
+ kasAllowNullAttributes = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment', 'idTestSet' ];
+
+ kcDbColumns = 7;
+
+ def __init__(self):
+ ModelDataBase.__init__(self)
+ self.idTestResult = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.idTestSet = None;
+ self.idFailureReason = None;
+ self.sComment = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestResultFailures.
+ Return self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test result file record not found.')
+
+ self.idTestResult = aoRow[0];
+ self.tsEffective = aoRow[1];
+ self.tsExpire = aoRow[2];
+ self.uidAuthor = aoRow[3];
+ self.idTestSet = aoRow[4];
+ self.idFailureReason = aoRow[5];
+ self.sComment = aoRow[6];
+ return self;
+
+ def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM TestResultFailures\n'
+ 'WHERE idTestResult = %s\n'
+ , ( idTestResult,), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idTestResult=%s not found (tsNow=%s, sPeriodBack=%s)' % (idTestResult, tsNow, sPeriodBack));
+ assert len(aoRow) == self.kcDbColumns;
+ return self.initFromDbRow(aoRow);
+
+ def initFromValues(self, idTestResult, idFailureReason, uidAuthor,
+ tsExpire = None, tsEffective = None, idTestSet = None, sComment = None):
+ """
+ Initialize from values.
+ """
+ self.idTestResult = idTestResult;
+ self.tsEffective = tsEffective;
+ self.tsExpire = tsExpire;
+ self.uidAuthor = uidAuthor;
+ self.idTestSet = idTestSet;
+ self.idFailureReason = idFailureReason;
+ self.sComment = sComment;
+ return self;
+
+
+
+class TestResultFailureDataEx(TestResultFailureData):
+ """
+ Extends TestResultFailureData by resolving reasons and user.
+ """
+
+ def __init__(self):
+ TestResultFailureData.__init__(self);
+ self.oFailureReason = None;
+ self.oAuthor = None;
+
+ def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
+ """
+ Reinitialize from a SELECT * FROM TestResultFailures.
+ Return self. Raises exception if no row.
+ """
+ self.initFromDbRow(aoRow);
+ self.oFailureReason = oFailureReasonLogic.cachedLookup(self.idFailureReason);
+ self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor);
+ return self;
+
+
+class TestResultListingData(ModelDataBase): # pylint: disable=too-many-instance-attributes
+ """
+ Test case result data representation for table listing
+ """
+
+ def __init__(self):
+ """Initialize"""
+ ModelDataBase.__init__(self)
+
+ self.idTestSet = None
+
+ self.idBuildCategory = None;
+ self.sProduct = None
+ self.sRepository = None;
+ self.sBranch = None
+ self.sType = None
+ self.idBuild = None;
+ self.sVersion = None;
+ self.iRevision = None
+
+ self.sOs = None;
+ self.sOsVersion = None;
+ self.sArch = None;
+ self.sCpuVendor = None;
+ self.sCpuName = None;
+ self.cCpus = None;
+ self.fCpuHwVirt = None;
+ self.fCpuNestedPaging = None;
+ self.fCpu64BitGuest = None;
+ self.idTestBox = None
+ self.sTestBoxName = None
+
+ self.tsCreated = None
+ self.tsElapsed = None
+ self.enmStatus = None
+ self.cErrors = None;
+
+ self.idTestCase = None
+ self.sTestCaseName = None
+ self.sBaseCmd = None
+ self.sArgs = None
+ self.sSubName = None;
+
+ self.idBuildTestSuite = None;
+ self.iRevisionTestSuite = None;
+
+ self.oFailureReason = None;
+ self.oFailureReasonAssigner = None;
+ self.tsFailureReasonAssigned = None;
+ self.sFailureReasonComment = None;
+
+ def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
+ """
+ Reinitialize from a database query.
+ Return self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test result record not found.')
+
+ self.idTestSet = aoRow[0];
+
+ self.idBuildCategory = aoRow[1];
+ self.sProduct = aoRow[2];
+ self.sRepository = aoRow[3];
+ self.sBranch = aoRow[4];
+ self.sType = aoRow[5];
+ self.idBuild = aoRow[6];
+ self.sVersion = aoRow[7];
+ self.iRevision = aoRow[8];
+
+ self.sOs = aoRow[9];
+ self.sOsVersion = aoRow[10];
+ self.sArch = aoRow[11];
+ self.sCpuVendor = aoRow[12];
+ self.sCpuName = aoRow[13];
+ self.cCpus = aoRow[14];
+ self.fCpuHwVirt = aoRow[15];
+ self.fCpuNestedPaging = aoRow[16];
+ self.fCpu64BitGuest = aoRow[17];
+ self.idTestBox = aoRow[18];
+ self.sTestBoxName = aoRow[19];
+
+ self.tsCreated = aoRow[20];
+ self.tsElapsed = aoRow[21];
+ self.enmStatus = aoRow[22];
+ self.cErrors = aoRow[23];
+
+ self.idTestCase = aoRow[24];
+ self.sTestCaseName = aoRow[25];
+ self.sBaseCmd = aoRow[26];
+ self.sArgs = aoRow[27];
+ self.sSubName = aoRow[28];
+
+ self.idBuildTestSuite = aoRow[29];
+ self.iRevisionTestSuite = aoRow[30];
+
+ self.oFailureReason = None;
+ if aoRow[31] is not None:
+ self.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[31]);
+ self.oFailureReasonAssigner = None;
+ if aoRow[32] is not None:
+ self.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[32]);
+ self.tsFailureReasonAssigned = aoRow[33];
+ self.sFailureReasonComment = aoRow[34];
+
+ return self
+
+
+
+class TestResultFailureLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Test result failure reason logic.
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+
+ def fetchForChangeLog(self, idTestResult, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals
+ """
+ Fetches change log entries for a failure reason.
+
+ Returns an array of ChangeLogEntry instance and an indicator whether
+ there are more entries.
+ Raises exception on error.
+ """
+
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+
+ # 1. Get a list of the changes from both TestResultFailures and assoicated
+ # FailureReasons. The latter is useful since the failure reason
+ # description may evolve along side the invidiual failure analysis.
+ self._oDb.execute('( SELECT trf.tsEffective AS tsEffectiveChangeLog,\n'
+ ' trf.uidAuthor AS uidAuthorChangeLog,\n'
+ ' trf.*,\n'
+ ' fr.*\n'
+ ' FROM TestResultFailures trf,\n'
+ ' FailureReasons fr\n'
+ ' WHERE trf.idTestResult = %s\n'
+ ' AND trf.tsEffective <= %s\n'
+ ' AND trf.idFailureReason = fr.idFailureReason\n'
+ ' AND fr.tsEffective <= trf.tsEffective\n'
+ ' AND fr.tsExpire > trf.tsEffective\n'
+ ')\n'
+ 'UNION\n'
+ '( SELECT fr.tsEffective AS tsEffectiveChangeLog,\n'
+ ' fr.uidAuthor AS uidAuthorChangeLog,\n'
+ ' trf.*,\n'
+ ' fr.*\n'
+ ' FROM TestResultFailures trf,\n'
+ ' FailureReasons fr\n'
+ ' WHERE trf.idTestResult = %s\n'
+ ' AND trf.tsEffective <= %s\n'
+ ' AND trf.idFailureReason = fr.idFailureReason\n'
+ ' AND fr.tsEffective > trf.tsEffective\n'
+ ' AND fr.tsEffective < trf.tsExpire\n'
+ ')\n'
+ 'ORDER BY tsEffectiveChangeLog DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , ( idTestResult, tsNow, idTestResult, tsNow, cMaxRows + 1, iStart, ));
+
+ aaoRows = [];
+ for aoChange in self._oDb.fetchAll():
+ oTrf = TestResultFailureDataEx().initFromDbRow(aoChange[2:]);
+ oFr = FailureReasonData().initFromDbRow(aoChange[(2+TestResultFailureData.kcDbColumns):]);
+ oTrf.oFailureReason = oFr;
+ aaoRows.append([aoChange[0], aoChange[1], oTrf, oFr]);
+
+ # 2. Calculate the changes.
+ oFailureCategoryLogic = None;
+ aoEntries = [];
+ for i in xrange(0, len(aaoRows) - 1):
+ aoNew = aaoRows[i];
+ aoOld = aaoRows[i + 1];
+
+ aoChanges = [];
+ oNew = aoNew[2];
+ oOld = aoOld[2];
+ for sAttr in oNew.getDataAttributes():
+ if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', 'oFailureReason', 'oAuthor' ]:
+ oOldAttr = getattr(oOld, sAttr);
+ oNewAttr = getattr(oNew, sAttr);
+ if oOldAttr != oNewAttr:
+ if sAttr == 'idFailureReason':
+ oNewAttr = '%s (%s)' % (oNewAttr, oNew.oFailureReason.sShort, );
+ oOldAttr = '%s (%s)' % (oOldAttr, oOld.oFailureReason.sShort, );
+ aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+ if oOld.idFailureReason == oNew.idFailureReason:
+ oNew = aoNew[3];
+ oOld = aoOld[3];
+ for sAttr in oNew.getDataAttributes():
+ if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
+ oOldAttr = getattr(oOld, sAttr);
+ oNewAttr = getattr(oNew, sAttr);
+ if oOldAttr != oNewAttr:
+ if sAttr == 'idFailureCategory':
+ if oFailureCategoryLogic is None:
+ from testmanager.core.failurecategory import FailureCategoryLogic;
+ oFailureCategoryLogic = FailureCategoryLogic(self._oDb);
+ oCat = oFailureCategoryLogic.cachedLookup(oNewAttr);
+ if oCat is not None:
+ oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
+ oCat = oFailureCategoryLogic.cachedLookup(oOldAttr);
+ if oCat is not None:
+ oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
+ aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
+
+
+ tsExpire = aaoRows[i - 1][0] if i > 0 else aoNew[2].tsExpire;
+ aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], aoOld[2], aoChanges));
+
+ # If we're at the end of the log, add the initial entry.
+ if len(aaoRows) <= cMaxRows and aaoRows:
+ aoNew = aaoRows[-1];
+ tsExpire = aaoRows[-1 - 1][0] if len(aaoRows) > 1 else aoNew[2].tsExpire;
+ aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], None, []));
+
+ return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aaoRows) > cMaxRows);
+
+
+ def getById(self, idTestResult):
+ """Get Test result failure reason data by idTestResult"""
+
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestResultFailures\n'
+ 'WHERE tsExpire = \'infinity\'::timestamp\n'
+ ' AND idTestResult = %s;', (idTestResult,))
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise self._oDb.integrityException(
+ 'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
+ try:
+ return TestResultFailureData().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Add a test result failure reason record.
+ """
+
+ #
+ # Validate inputs and read in the old(/current) data.
+ #
+ assert isinstance(oData, TestResultFailureData);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_AddForeignId);
+ if dErrors:
+ raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
+
+ # Check if it exist first (we're adding, not editing, collisions not allowed).
+ oOldData = self.getById(oData.idTestResult);
+ if oOldData is not None:
+ raise TMRowAlreadyExists('TestResult %d already have a failure reason associated with it:'
+ '%s\n'
+ 'Perhaps someone else beat you to it? Or did you try resubmit?'
+ % (oData.idTestResult, oOldData));
+ oData = self._resolveSetTestIdIfMissing(oData);
+
+ #
+ # Add record.
+ #
+ self._readdEntry(uidAuthor, oData);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Modifies a test result failure reason.
+ """
+
+ #
+ # Validate inputs and read in the old(/current) data.
+ #
+ assert isinstance(oData, TestResultFailureData);
+ dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
+ if dErrors:
+ raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
+
+ oOldData = self.getById(oData.idTestResult)
+ oData.idTestSet = oOldData.idTestSet;
+
+ #
+ # Update the data that needs updating.
+ #
+ if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
+ self._historizeEntry(oData.idTestResult);
+ self._readdEntry(uidAuthor, oData);
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def removeEntry(self, uidAuthor, idTestResult, fCascade = False, fCommit = False):
+ """
+ Deletes a test result failure reason.
+ """
+ _ = fCascade; # Not applicable.
+
+ oData = self.getById(idTestResult)
+ (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
+ if oData.tsEffective not in (tsCur, tsCurMinusOne):
+ self._historizeEntry(idTestResult, tsCurMinusOne);
+ self._readdEntry(uidAuthor, oData, tsCurMinusOne);
+ self._historizeEntry(idTestResult);
+ self._oDb.execute('UPDATE TestResultFailures\n'
+ 'SET tsExpire = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestResult = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (idTestResult,));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ #
+ # Helpers.
+ #
+
+ def _readdEntry(self, uidAuthor, oData, tsEffective = None):
+ """
+ Re-adds the TestResultFailure entry. Used by addEntry, editEntry and removeEntry.
+ """
+ if tsEffective is None:
+ tsEffective = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('INSERT INTO TestResultFailures (\n'
+ ' uidAuthor,\n'
+ ' tsEffective,\n'
+ ' idTestResult,\n'
+ ' idTestSet,\n'
+ ' idFailureReason,\n'
+ ' sComment)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s)\n'
+ , ( uidAuthor,
+ tsEffective,
+ oData.idTestResult,
+ oData.idTestSet,
+ oData.idFailureReason,
+ oData.sComment,) );
+ return True;
+
+
+ def _historizeEntry(self, idTestResult, tsExpire = None):
+ """ Historizes the current entry. """
+ if tsExpire is None:
+ tsExpire = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('UPDATE TestResultFailures\n'
+ 'SET tsExpire = %s\n'
+ 'WHERE idTestResult = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (tsExpire, idTestResult,));
+ return True;
+
+
+ def _resolveSetTestIdIfMissing(self, oData):
+ """ Resolve any missing idTestSet reference (it's a duplicate for speed efficiency). """
+ if oData.idTestSet is None and oData.idTestResult is not None:
+ self._oDb.execute('SELECT idTestSet FROM TestResults WHERE idTestResult = %s', (oData.idTestResult,));
+ oData.idTestSet = self._oDb.fetchOne()[0];
+ return oData;
+
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class TestResultFailureDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestResultFailureData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testresults.py b/src/VBox/ValidationKit/testmanager/core/testresults.py
new file mode 100755
index 00000000..58f056d2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testresults.py
@@ -0,0 +1,2926 @@
+# -*- coding: utf-8 -*-
+# $Id: testresults.py $
+# pylint: disable=too-many-lines
+
+## @todo Rename this file to testresult.py!
+
+"""
+Test Manager - Fetch test results.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import sys;
+import unittest;
+
+# Validation Kit imports.
+from common import constants;
+from testmanager import config;
+from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, ModelFilterBase, \
+ FilterCriterion, FilterCriterionValueAndDescription, \
+ TMExceptionBase, TMTooManyRows, TMRowNotFound;
+from testmanager.core.testgroup import TestGroupData;
+from testmanager.core.build import BuildDataEx, BuildCategoryData;
+from testmanager.core.failurereason import FailureReasonLogic;
+from testmanager.core.testbox import TestBoxData, TestBoxLogic;
+from testmanager.core.testcase import TestCaseData;
+from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic;
+from testmanager.core.systemlog import SystemLogData, SystemLogLogic;
+from testmanager.core.testresultfailures import TestResultFailureDataEx;
+from testmanager.core.useraccount import UserAccountLogic;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ long = int; # pylint: disable=redefined-builtin,invalid-name
+
+
+class TestResultData(ModelDataBase):
+ """
+ Test case execution result data
+ """
+
+ ## @name TestStatus_T
+ # @{
+ ksTestStatus_Running = 'running';
+ ksTestStatus_Success = 'success';
+ ksTestStatus_Skipped = 'skipped';
+ ksTestStatus_BadTestBox = 'bad-testbox';
+ ksTestStatus_Aborted = 'aborted';
+ ksTestStatus_Failure = 'failure';
+ ksTestStatus_TimedOut = 'timed-out';
+ ksTestStatus_Rebooted = 'rebooted';
+ ## @}
+
+ ## List of relatively harmless (to testgroup/case) statuses.
+ kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ];
+ ## List of bad statuses.
+ kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ];
+
+
+ ksIdAttr = 'idTestResult';
+
+ ksParam_idTestResult = 'TestResultData_idTestResult';
+ ksParam_idTestResultParent = 'TestResultData_idTestResultParent';
+ ksParam_idTestSet = 'TestResultData_idTestSet';
+ ksParam_tsCreated = 'TestResultData_tsCreated';
+ ksParam_tsElapsed = 'TestResultData_tsElapsed';
+ ksParam_idStrName = 'TestResultData_idStrName';
+ ksParam_cErrors = 'TestResultData_cErrors';
+ ksParam_enmStatus = 'TestResultData_enmStatus';
+ ksParam_iNestingDepth = 'TestResultData_iNestingDepth';
+ kasValidValues_enmStatus = [
+ ksTestStatus_Running,
+ ksTestStatus_Success,
+ ksTestStatus_Skipped,
+ ksTestStatus_BadTestBox,
+ ksTestStatus_Aborted,
+ ksTestStatus_Failure,
+ ksTestStatus_TimedOut,
+ ksTestStatus_Rebooted
+ ];
+
+
+ def __init__(self):
+ ModelDataBase.__init__(self)
+ self.idTestResult = None
+ self.idTestResultParent = None
+ self.idTestSet = None
+ self.tsCreated = None
+ self.tsElapsed = None
+ self.idStrName = None
+ self.cErrors = 0;
+ self.enmStatus = None
+ self.iNestingDepth = None
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestResults.
+ Return self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test result record not found.')
+
+ self.idTestResult = aoRow[0]
+ self.idTestResultParent = aoRow[1]
+ self.idTestSet = aoRow[2]
+ self.tsCreated = aoRow[3]
+ self.tsElapsed = aoRow[4]
+ self.idStrName = aoRow[5]
+ self.cErrors = aoRow[6]
+ self.enmStatus = aoRow[7]
+ self.iNestingDepth = aoRow[8]
+ return self;
+
+ def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):
+ """
+ Initialize from the database, given the ID of a row.
+ """
+ _ = tsNow;
+ _ = sPeriodBack;
+ oDb.execute('SELECT *\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestResult = %s\n'
+ , ( idTestResult,));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idTestResult=%s not found' % (idTestResult,));
+ return self.initFromDbRow(aoRow);
+
+ def isFailure(self):
+ """ Check if it's a real failure. """
+ return self.enmStatus in self.kasBadTestStatuses;
+
+
+class TestResultDataEx(TestResultData):
+ """
+ Extended test result data class.
+
+ This is intended for use as a node in a result tree. This is not intended
+ for serialization to parameters or vice versa. Use TestResultLogic to
+ construct the tree.
+ """
+
+ def __init__(self):
+ TestResultData.__init__(self)
+ self.sName = None; # idStrName resolved.
+ self.oParent = None; # idTestResultParent within the tree.
+
+ self.aoChildren = []; # TestResultDataEx;
+ self.aoValues = []; # TestResultValueDataEx;
+ self.aoMsgs = []; # TestResultMsgDataEx;
+ self.aoFiles = []; # TestResultFileDataEx;
+ self.oReason = None; # TestResultReasonDataEx;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Initialize from a query like this:
+ SELECT TestResults.*, TestResultStrTab.sValue
+ FROM TestResults, TestResultStrTab
+ WHERE TestResultStrTab.idStr = TestResults.idStrName
+
+ Note! The caller is expected to fetch children, values, failure
+ details, and files.
+ """
+ self.sName = None;
+ self.oParent = None;
+ self.aoChildren = [];
+ self.aoValues = [];
+ self.aoMsgs = [];
+ self.aoFiles = [];
+ self.oReason = None;
+
+ TestResultData.initFromDbRow(self, aoRow);
+
+ self.sName = aoRow[9];
+ return self;
+
+ def deepCountErrorContributers(self):
+ """
+ Counts how many test result instances actually contributed to cErrors.
+ """
+
+ # Check each child (if any).
+ cChanges = 0;
+ cChildErrors = 0;
+ for oChild in self.aoChildren:
+ if oChild.cErrors > 0:
+ cChildErrors += oChild.cErrors;
+ cChanges += oChild.deepCountErrorContributers();
+
+ # Did we contribute as well?
+ if self.cErrors > cChildErrors:
+ cChanges += 1;
+ return cChanges;
+
+ def getListOfFailures(self):
+ """
+ Get a list of test results instances actually contributing to cErrors.
+
+ Returns a list of TestResultDataEx instances from this tree. (shared!)
+ """
+ # Check each child (if any).
+ aoRet = [];
+ cChildErrors = 0;
+ for oChild in self.aoChildren:
+ if oChild.cErrors > 0:
+ cChildErrors += oChild.cErrors;
+ aoRet.extend(oChild.getListOfFailures());
+
+ # Did we contribute as well?
+ if self.cErrors > cChildErrors:
+ aoRet.append(self);
+
+ return aoRet;
+
+ def getListOfLogFilesByKind(self, asKinds):
+ """
+ Get a list of test results instances actually contributing to cErrors.
+
+ Returns a list of TestResultFileDataEx instances from this tree. (shared!)
+ """
+ aoRet = [];
+
+ # Check the children first.
+ for oChild in self.aoChildren:
+ aoRet.extend(oChild.getListOfLogFilesByKind(asKinds));
+
+ # Check our own files next.
+ for oFile in self.aoFiles:
+ if oFile.sKind in asKinds:
+ aoRet.append(oFile);
+
+ return aoRet;
+
+ def getFullName(self):
+ """ Constructs the full name of this test result. """
+ if self.oParent is None:
+ return self.sName;
+ return self.oParent.getFullName() + ' / ' + self.sName;
+
+
+
+class TestResultValueData(ModelDataBase):
+ """
+ Test result value data.
+ """
+
+ ksIdAttr = 'idTestResultValue';
+
+ ksParam_idTestResultValue = 'TestResultValue_idTestResultValue';
+ ksParam_idTestResult = 'TestResultValue_idTestResult';
+ ksParam_idTestSet = 'TestResultValue_idTestSet';
+ ksParam_tsCreated = 'TestResultValue_tsCreated';
+ ksParam_idStrName = 'TestResultValue_idStrName';
+ ksParam_lValue = 'TestResultValue_lValue';
+ ksParam_iUnit = 'TestResultValue_iUnit';
+
+ kasAllowNullAttributes = [ 'idTestSet', ];
+
+ def __init__(self):
+ ModelDataBase.__init__(self)
+ self.idTestResultValue = None;
+ self.idTestResult = None;
+ self.idTestSet = None;
+ self.tsCreated = None;
+ self.idStrName = None;
+ self.lValue = None;
+ self.iUnit = 0;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestResultValues.
+ Return self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test result value record not found.')
+
+ self.idTestResultValue = aoRow[0];
+ self.idTestResult = aoRow[1];
+ self.idTestSet = aoRow[2];
+ self.tsCreated = aoRow[3];
+ self.idStrName = aoRow[4];
+ self.lValue = aoRow[5];
+ self.iUnit = aoRow[6];
+ return self;
+
+
+class TestResultValueDataEx(TestResultValueData):
+ """
+ Extends TestResultValue by resolving the value name and unit string.
+ """
+
+ def __init__(self):
+ TestResultValueData.__init__(self)
+ self.sName = None;
+ self.sUnit = '';
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a query like this:
+ SELECT TestResultValues.*, TestResultStrTab.sValue
+ FROM TestResultValues, TestResultStrTab
+ WHERE TestResultStrTab.idStr = TestResultValues.idStrName
+
+ Return self. Raises exception if no row.
+ """
+ TestResultValueData.initFromDbRow(self, aoRow);
+ self.sName = aoRow[7];
+ if self.iUnit < len(constants.valueunit.g_asNames):
+ self.sUnit = constants.valueunit.g_asNames[self.iUnit];
+ else:
+ self.sUnit = '<%d>' % (self.iUnit,);
+ return self;
+
+class TestResultMsgData(ModelDataBase):
+ """
+ Test result message data.
+ """
+
+ ksIdAttr = 'idTestResultMsg';
+
+ ksParam_idTestResultMsg = 'TestResultValue_idTestResultMsg';
+ ksParam_idTestResult = 'TestResultValue_idTestResult';
+ ksParam_idTestSet = 'TestResultValue_idTestSet';
+ ksParam_tsCreated = 'TestResultValue_tsCreated';
+ ksParam_idStrMsg = 'TestResultValue_idStrMsg';
+ ksParam_enmLevel = 'TestResultValue_enmLevel';
+
+ kasAllowNullAttributes = [ 'idTestSet', ];
+
+ kcDbColumns = 6
+
+ def __init__(self):
+ ModelDataBase.__init__(self)
+ self.idTestResultMsg = None;
+ self.idTestResult = None;
+ self.idTestSet = None;
+ self.tsCreated = None;
+ self.idStrMsg = None;
+ self.enmLevel = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestResultMsgs.
+ Return self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test result value record not found.')
+
+ self.idTestResultMsg = aoRow[0];
+ self.idTestResult = aoRow[1];
+ self.idTestSet = aoRow[2];
+ self.tsCreated = aoRow[3];
+ self.idStrMsg = aoRow[4];
+ self.enmLevel = aoRow[5];
+ return self;
+
+class TestResultMsgDataEx(TestResultMsgData):
+ """
+ Extends TestResultMsg by resolving the message string.
+ """
+
+ def __init__(self):
+ TestResultMsgData.__init__(self)
+ self.sMsg = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a query like this:
+ SELECT TestResultMsg.*, TestResultStrTab.sValue
+ FROM TestResultMsg, TestResultStrTab
+ WHERE TestResultStrTab.idStr = TestResultMsgs.idStrName
+
+ Return self. Raises exception if no row.
+ """
+ TestResultMsgData.initFromDbRow(self, aoRow);
+ self.sMsg = aoRow[self.kcDbColumns];
+ return self;
+
+
+class TestResultFileData(ModelDataBase):
+ """
+ Test result message data.
+ """
+
+ ksIdAttr = 'idTestResultFile';
+
+ ksParam_idTestResultFile = 'TestResultFile_idTestResultFile';
+ ksParam_idTestResult = 'TestResultFile_idTestResult';
+ ksParam_tsCreated = 'TestResultFile_tsCreated';
+ ksParam_idStrFile = 'TestResultFile_idStrFile';
+ ksParam_idStrDescription = 'TestResultFile_idStrDescription';
+ ksParam_idStrKind = 'TestResultFile_idStrKind';
+ ksParam_idStrMime = 'TestResultFile_idStrMime';
+
+ ## @name Kind of files.
+ ## @{
+ ksKind_LogReleaseVm = 'log/release/vm';
+ ksKind_LogDebugVm = 'log/debug/vm';
+ ksKind_LogReleaseSvc = 'log/release/svc';
+ ksKind_LogDebugSvc = 'log/debug/svc';
+ ksKind_LogReleaseClient = 'log/release/client';
+ ksKind_LogDebugClient = 'log/debug/client';
+ ksKind_LogInstaller = 'log/installer';
+ ksKind_LogUninstaller = 'log/uninstaller';
+ ksKind_LogGuestKernel = 'log/guest/kernel';
+ ksKind_ProcessReportVm = 'process/report/vm';
+ ksKind_CrashReportVm = 'crash/report/vm';
+ ksKind_CrashDumpVm = 'crash/dump/vm';
+ ksKind_CrashReportSvc = 'crash/report/svc';
+ ksKind_CrashDumpSvc = 'crash/dump/svc';
+ ksKind_CrashReportClient = 'crash/report/client';
+ ksKind_CrashDumpClient = 'crash/dump/client';
+ ksKind_InfoCollection = 'info/collection';
+ ksKind_InfoVgaText = 'info/vgatext';
+ ksKind_MiscOther = 'misc/other';
+ ksKind_ScreenshotFailure = 'screenshot/failure';
+ ksKind_ScreenshotSuccesss = 'screenshot/success';
+ ksKind_ScreenRecordingFailure = 'screenrecording/failure';
+ ksKind_ScreenRecordingSuccess = 'screenrecording/success';
+ ## @}
+
+ kasKinds = [
+ ksKind_LogReleaseVm,
+ ksKind_LogDebugVm,
+ ksKind_LogReleaseSvc,
+ ksKind_LogDebugSvc,
+ ksKind_LogReleaseClient,
+ ksKind_LogDebugClient,
+ ksKind_LogInstaller,
+ ksKind_LogUninstaller,
+ ksKind_LogGuestKernel,
+ ksKind_ProcessReportVm,
+ ksKind_CrashReportVm,
+ ksKind_CrashDumpVm,
+ ksKind_CrashReportSvc,
+ ksKind_CrashDumpSvc,
+ ksKind_CrashReportClient,
+ ksKind_CrashDumpClient,
+ ksKind_InfoCollection,
+ ksKind_InfoVgaText,
+ ksKind_MiscOther,
+ ksKind_ScreenshotFailure,
+ ksKind_ScreenshotSuccesss,
+ ksKind_ScreenRecordingFailure,
+ ksKind_ScreenRecordingSuccess,
+ ];
+
+ kasAllowNullAttributes = [ 'idTestSet', ];
+
+ kcDbColumns = 8
+
+ def __init__(self):
+ ModelDataBase.__init__(self)
+ self.idTestResultFile = None;
+ self.idTestResult = None;
+ self.idTestSet = None;
+ self.tsCreated = None;
+ self.idStrFile = None;
+ self.idStrDescription = None;
+ self.idStrKind = None;
+ self.idStrMime = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a SELECT * FROM TestResultFiles.
+ Return self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test result file record not found.')
+
+ self.idTestResultFile = aoRow[0];
+ self.idTestResult = aoRow[1];
+ self.idTestSet = aoRow[2];
+ self.tsCreated = aoRow[3];
+ self.idStrFile = aoRow[4];
+ self.idStrDescription = aoRow[5];
+ self.idStrKind = aoRow[6];
+ self.idStrMime = aoRow[7];
+ return self;
+
+class TestResultFileDataEx(TestResultFileData):
+ """
+ Extends TestResultFile by resolving the strings.
+ """
+
+ def __init__(self):
+ TestResultFileData.__init__(self)
+ self.sFile = None;
+ self.sDescription = None;
+ self.sKind = None;
+ self.sMime = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Reinitialize from a query like this:
+ SELECT TestResultFiles.*,
+ StrTabFile.sValue AS sFile,
+ StrTabDesc.sValue AS sDescription
+ StrTabKind.sValue AS sKind,
+ StrTabMime.sValue AS sMime,
+ FROM ...
+
+ Return self. Raises exception if no row.
+ """
+ TestResultFileData.initFromDbRow(self, aoRow);
+ self.sFile = aoRow[self.kcDbColumns];
+ self.sDescription = aoRow[self.kcDbColumns + 1];
+ self.sKind = aoRow[self.kcDbColumns + 2];
+ self.sMime = aoRow[self.kcDbColumns + 3];
+ return self;
+
+ def initFakeMainLog(self, oTestSet):
+ """
+ Reinitializes to represent the main.log object (not in DB).
+
+ Returns self.
+ """
+ self.idTestResultFile = 0;
+ self.idTestResult = oTestSet.idTestResult;
+ self.tsCreated = oTestSet.tsCreated;
+ self.idStrFile = None;
+ self.idStrDescription = None;
+ self.idStrKind = None;
+ self.idStrMime = None;
+
+ self.sFile = 'main.log';
+ self.sDescription = '';
+ self.sKind = 'log/main';
+ self.sMime = 'text/plain';
+ return self;
+
+ def isProbablyUtf8Encoded(self):
+ """
+ Checks if the file is likely to be UTF-8 encoded.
+ """
+ if self.sMime in [ 'text/plain', 'text/html' ]:
+ return True;
+ return False;
+
+ def getMimeWithEncoding(self):
+ """
+ Gets the MIME type with encoding if likely to be UTF-8.
+ """
+ if self.isProbablyUtf8Encoded():
+ return '%s; charset=utf-8' % (self.sMime,);
+ return self.sMime;
+
+
+
+class TestResultListingData(ModelDataBase): # pylint: disable=too-many-instance-attributes
+ """
+ Test case result data representation for table listing
+ """
+
+ class FailureReasonListingData(object):
+ """ Failure reason listing data """
+ def __init__(self):
+ self.oFailureReason = None;
+ self.oFailureReasonAssigner = None;
+ self.tsFailureReasonAssigned = None;
+ self.sFailureReasonComment = None;
+
+ def __init__(self):
+ """Initialize"""
+ ModelDataBase.__init__(self)
+
+ self.idTestSet = None
+
+ self.idBuildCategory = None;
+ self.sProduct = None
+ self.sRepository = None;
+ self.sBranch = None
+ self.sType = None
+ self.idBuild = None;
+ self.sVersion = None;
+ self.iRevision = None
+
+ self.sOs = None;
+ self.sOsVersion = None;
+ self.sArch = None;
+ self.sCpuVendor = None;
+ self.sCpuName = None;
+ self.cCpus = None;
+ self.fCpuHwVirt = None;
+ self.fCpuNestedPaging = None;
+ self.fCpu64BitGuest = None;
+ self.idTestBox = None
+ self.sTestBoxName = None
+
+ self.tsCreated = None
+ self.tsElapsed = None
+ self.enmStatus = None
+ self.cErrors = None;
+
+ self.idTestCase = None
+ self.sTestCaseName = None
+ self.sBaseCmd = None
+ self.sArgs = None
+ self.sSubName = None;
+
+ self.idBuildTestSuite = None;
+ self.iRevisionTestSuite = None;
+
+ self.aoFailureReasons = [];
+
+ def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
+ """
+ Reinitialize from a database query.
+ Return self. Raises exception if no row.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('Test result record not found.')
+
+ self.idTestSet = aoRow[0];
+
+ self.idBuildCategory = aoRow[1];
+ self.sProduct = aoRow[2];
+ self.sRepository = aoRow[3];
+ self.sBranch = aoRow[4];
+ self.sType = aoRow[5];
+ self.idBuild = aoRow[6];
+ self.sVersion = aoRow[7];
+ self.iRevision = aoRow[8];
+
+ self.sOs = aoRow[9];
+ self.sOsVersion = aoRow[10];
+ self.sArch = aoRow[11];
+ self.sCpuVendor = aoRow[12];
+ self.sCpuName = aoRow[13];
+ self.cCpus = aoRow[14];
+ self.fCpuHwVirt = aoRow[15];
+ self.fCpuNestedPaging = aoRow[16];
+ self.fCpu64BitGuest = aoRow[17];
+ self.idTestBox = aoRow[18];
+ self.sTestBoxName = aoRow[19];
+
+ self.tsCreated = aoRow[20];
+ self.tsElapsed = aoRow[21];
+ self.enmStatus = aoRow[22];
+ self.cErrors = aoRow[23];
+
+ self.idTestCase = aoRow[24];
+ self.sTestCaseName = aoRow[25];
+ self.sBaseCmd = aoRow[26];
+ self.sArgs = aoRow[27];
+ self.sSubName = aoRow[28];
+
+ self.idBuildTestSuite = aoRow[29];
+ self.iRevisionTestSuite = aoRow[30];
+
+ self.aoFailureReasons = [];
+ for i, _ in enumerate(aoRow[31]):
+ if aoRow[31][i] is not None \
+ or aoRow[32][i] is not None \
+ or aoRow[33][i] is not None \
+ or aoRow[34][i] is not None:
+ oReason = self.FailureReasonListingData();
+ if aoRow[31][i] is not None:
+ oReason.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[31][i]);
+ if aoRow[32][i] is not None:
+ oReason.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[32][i]);
+ oReason.tsFailureReasonAssigned = aoRow[33][i];
+ oReason.sFailureReasonComment = aoRow[34][i];
+ self.aoFailureReasons.append(oReason);
+
+ return self
+
+
+class TestResultHangingOffence(TMExceptionBase):
+ """Hanging offence committed by test case."""
+ pass; # pylint: disable=unnecessary-pass
+
+
+class TestResultFilter(ModelFilterBase):
+ """
+ Test result filter.
+ """
+
+ kiTestStatus = 0;
+ kiErrorCounts = 1;
+ kiBranches = 2;
+ kiBuildTypes = 3;
+ kiRevisions = 4;
+ kiRevisionRange = 5;
+ kiFailReasons = 6;
+ kiTestCases = 7;
+ kiTestCaseMisc = 8;
+ kiTestBoxes = 9;
+ kiOses = 10;
+ kiCpuArches = 11;
+ kiCpuVendors = 12;
+ kiCpuCounts = 13;
+ kiMemory = 14;
+ kiTestboxMisc = 15;
+ kiPythonVersions = 16;
+ kiSchedGroups = 17;
+
+ ## Misc test case / variation name filters.
+ ## Presented in table order. The first sub element is the presistent ID.
+ kaTcMisc = (
+ ( 1, 'x86', ),
+ ( 2, 'amd64', ),
+ ( 3, 'uni', ),
+ ( 4, 'smp', ),
+ ( 5, 'raw', ),
+ ( 6, 'hw', ),
+ ( 7, 'np', ),
+ ( 8, 'Install', ),
+ ( 20, 'UInstall', ), # NB. out of order.
+ ( 9, 'Benchmark', ),
+ ( 18, 'smoke', ), # NB. out of order.
+ ( 19, 'unit', ), # NB. out of order.
+ ( 10, 'USB', ),
+ ( 11, 'Debian', ),
+ ( 12, 'Fedora', ),
+ ( 13, 'Oracle', ),
+ ( 14, 'RHEL', ),
+ ( 15, 'SUSE', ),
+ ( 16, 'Ubuntu', ),
+ ( 17, 'Win', ),
+ );
+
+ kiTbMisc_NestedPaging = 0;
+ kiTbMisc_NoNestedPaging = 1;
+ kiTbMisc_RawMode = 2;
+ kiTbMisc_NoRawMode = 3;
+ kiTbMisc_64BitGuest = 4;
+ kiTbMisc_No64BitGuest = 5;
+ kiTbMisc_HwVirt = 6;
+ kiTbMisc_NoHwVirt = 7;
+ kiTbMisc_IoMmu = 8;
+ kiTbMisc_NoIoMmu = 9;
+
+ def __init__(self):
+ ModelFilterBase.__init__(self);
+
+ # Test statuses
+ oCrit = FilterCriterion('Test statuses', sVarNm = 'ts', sType = FilterCriterion.ksType_String,
+ sTable = 'TestSets', sColumn = 'enmStatus');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiTestStatus] is oCrit;
+
+ # Error counts
+ oCrit = FilterCriterion('Error counts', sVarNm = 'ec', sTable = 'TestResults', sColumn = 'cErrors');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiErrorCounts] is oCrit;
+
+ # Branches
+ oCrit = FilterCriterion('Branches', sVarNm = 'br', sType = FilterCriterion.ksType_String,
+ sTable = 'BuildCategories', sColumn = 'sBranch');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiBranches] is oCrit;
+
+ # Build types
+ oCrit = FilterCriterion('Build types', sVarNm = 'bt', sType = FilterCriterion.ksType_String,
+ sTable = 'BuildCategories', sColumn = 'sType');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiBuildTypes] is oCrit;
+
+ # Revisions
+ oCrit = FilterCriterion('Revisions', sVarNm = 'rv', sTable = 'Builds', sColumn = 'iRevision');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiRevisions] is oCrit;
+
+ # Revision Range
+ oCrit = FilterCriterion('Revision Range', sVarNm = 'rr', sType = FilterCriterion.ksType_Ranges,
+ sKind = FilterCriterion.ksKind_ElementOfOrNot, sTable = 'Builds', sColumn = 'iRevision');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiRevisionRange] is oCrit;
+
+ # Failure reasons
+ oCrit = FilterCriterion('Failure reasons', sVarNm = 'fr', sType = FilterCriterion.ksType_UIntNil,
+ sTable = 'TestResultFailures', sColumn = 'idFailureReason');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiFailReasons] is oCrit;
+
+ # Test cases and variations.
+ oCrit = FilterCriterion('Test case / var', sVarNm = 'tc', sTable = 'TestSets', sColumn = 'idTestCase',
+ oSub = FilterCriterion('Test variations', sVarNm = 'tv',
+ sTable = 'TestSets', sColumn = 'idTestCaseArgs'));
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiTestCases] is oCrit;
+
+ # Special test case and varation name sub string matching.
+ oCrit = FilterCriterion('Test case name', sVarNm = 'cm', sKind = FilterCriterion.ksKind_Special,
+ asTables = ('TestCases', 'TestCaseArgs'));
+ oCrit.aoPossible = [
+ FilterCriterionValueAndDescription(aoCur[0], 'Include %s' % (aoCur[1],)) for aoCur in self.kaTcMisc
+ ];
+ oCrit.aoPossible.extend([
+ FilterCriterionValueAndDescription(aoCur[0] + 32, 'Exclude %s' % (aoCur[1],)) for aoCur in self.kaTcMisc
+ ]);
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiTestCaseMisc] is oCrit;
+
+ # Testboxes
+ oCrit = FilterCriterion('Testboxes', sVarNm = 'tb', sTable = 'TestSets', sColumn = 'idTestBox');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiTestBoxes] is oCrit;
+
+ # Testbox OS and OS version.
+ oCrit = FilterCriterion('OS / version', sVarNm = 'os', sTable = 'TestBoxesWithStrings', sColumn = 'idStrOs',
+ oSub = FilterCriterion('OS Versions', sVarNm = 'ov',
+ sTable = 'TestBoxesWithStrings', sColumn = 'idStrOsVersion'));
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiOses] is oCrit;
+
+ # Testbox CPU architectures.
+ oCrit = FilterCriterion('CPU arches', sVarNm = 'ca', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuArch');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiCpuArches] is oCrit;
+
+ # Testbox CPU vendors and revisions.
+ oCrit = FilterCriterion('CPU vendor / rev', sVarNm = 'cv', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuVendor',
+ oSub = FilterCriterion('CPU revisions', sVarNm = 'cr',
+ sTable = 'TestBoxesWithStrings', sColumn = 'lCpuRevision'));
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiCpuVendors] is oCrit;
+
+ # Testbox CPU (thread) count
+ oCrit = FilterCriterion('CPU counts', sVarNm = 'cc', sTable = 'TestBoxesWithStrings', sColumn = 'cCpus');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiCpuCounts] is oCrit;
+
+ # Testbox memory sizes.
+ oCrit = FilterCriterion('Memory', sVarNm = 'mb', sTable = 'TestBoxesWithStrings', sColumn = 'cMbMemory');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiMemory] is oCrit;
+
+ # Testbox features.
+ oCrit = FilterCriterion('Testbox features', sVarNm = 'tm', sKind = FilterCriterion.ksKind_Special,
+ sTable = 'TestBoxesWithStrings');
+ oCrit.aoPossible = [
+ FilterCriterionValueAndDescription(self.kiTbMisc_NestedPaging, "req nested paging"),
+ FilterCriterionValueAndDescription(self.kiTbMisc_NoNestedPaging, "w/o nested paging"),
+ #FilterCriterionValueAndDescription(self.kiTbMisc_RawMode, "req raw-mode"), - not implemented yet.
+ #FilterCriterionValueAndDescription(self.kiTbMisc_NoRawMode, "w/o raw-mode"), - not implemented yet.
+ FilterCriterionValueAndDescription(self.kiTbMisc_64BitGuest, "req 64-bit guests"),
+ FilterCriterionValueAndDescription(self.kiTbMisc_No64BitGuest, "w/o 64-bit guests"),
+ FilterCriterionValueAndDescription(self.kiTbMisc_HwVirt, "req VT-x / AMD-V"),
+ FilterCriterionValueAndDescription(self.kiTbMisc_NoHwVirt, "w/o VT-x / AMD-V"),
+ #FilterCriterionValueAndDescription(self.kiTbMisc_IoMmu, "req I/O MMU"), - not implemented yet.
+ #FilterCriterionValueAndDescription(self.kiTbMisc_NoIoMmu, "w/o I/O MMU"), - not implemented yet.
+ ];
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiTestboxMisc] is oCrit;
+
+ # Testbox python versions.
+ oCrit = FilterCriterion('Python', sVarNm = 'py', sTable = 'TestBoxesWithStrings', sColumn = 'iPythonHexVersion');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiPythonVersions] is oCrit;
+
+ # Scheduling groups.
+ oCrit = FilterCriterion('Sched groups', sVarNm = 'sg', sTable = 'TestSets', sColumn = 'idSchedGroup');
+ self.aCriteria.append(oCrit);
+ assert self.aCriteria[self.kiSchedGroups] is oCrit;
+
+
+ kdTbMiscConditions = {
+ kiTbMisc_NestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging IS TRUE',
+ kiTbMisc_NoNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging IS FALSE',
+ kiTbMisc_RawMode: 'TestBoxesWithStrings.fRawMode IS TRUE',
+ kiTbMisc_NoRawMode: 'TestBoxesWithStrings.fRawMode IS NOT TRUE',
+ kiTbMisc_64BitGuest: 'TestBoxesWithStrings.fCpu64BitGuest IS TRUE',
+ kiTbMisc_No64BitGuest: 'TestBoxesWithStrings.fCpu64BitGuest IS FALSE',
+ kiTbMisc_HwVirt: 'TestBoxesWithStrings.fCpuHwVirt IS TRUE',
+ kiTbMisc_NoHwVirt: 'TestBoxesWithStrings.fCpuHwVirt IS FALSE',
+ kiTbMisc_IoMmu: 'TestBoxesWithStrings.fChipsetIoMmu IS TRUE',
+ kiTbMisc_NoIoMmu: 'TestBoxesWithStrings.fChipsetIoMmu IS FALSE',
+ };
+
+ def _getWhereWorker(self, iCrit, oCrit, sExtraIndent, iOmit):
+ """ Formats one - main or sub. """
+ sQuery = '';
+ if oCrit.sState == FilterCriterion.ksState_Selected and iCrit != iOmit:
+ if iCrit == self.kiTestCaseMisc:
+ for iValue, sLike in self.kaTcMisc:
+ if iValue in oCrit.aoSelected: sNot = '';
+ elif iValue + 32 in oCrit.aoSelected: sNot = 'NOT ';
+ else: continue;
+ sQuery += '%s AND %s (' % (sExtraIndent, sNot,);
+ if len(sLike) <= 3: # do word matching for small substrings (hw, np, smp, uni, ++).
+ sQuery += 'TestCases.sName ~ \'.*\\y%s\\y.*\' ' \
+ 'OR COALESCE(TestCaseArgs.sSubName, \'\') ~ \'.*\\y%s\\y.*\')\n' \
+ % ( sLike, sLike,);
+ else:
+ sQuery += 'TestCases.sName LIKE \'%%%s%%\' ' \
+ 'OR COALESCE(TestCaseArgs.sSubName, \'\') LIKE \'%%%s%%\')\n' \
+ % ( sLike, sLike,);
+ elif iCrit == self.kiTestboxMisc:
+ dConditions = self.kdTbMiscConditions;
+ for iValue in oCrit.aoSelected:
+ if iValue in dConditions:
+ sQuery += '%s AND %s\n' % (sExtraIndent, dConditions[iValue],);
+ elif oCrit.sType == FilterCriterion.ksType_Ranges:
+ assert not oCrit.aoPossible;
+ if oCrit.aoSelected:
+ asConditions = [];
+ for tRange in oCrit.aoSelected:
+ if tRange[0] == tRange[1]:
+ asConditions.append('%s.%s = %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[0]));
+ elif tRange[1] is None: # 9999-
+ asConditions.append('%s.%s >= %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[0]));
+ elif tRange[0] is None: # -9999
+ asConditions.append('%s.%s <= %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[1]));
+ else:
+ asConditions.append('%s.%s BETWEEN %s AND %s' % (oCrit.asTables[0], oCrit.sColumn,
+ tRange[0], tRange[1]));
+ if not oCrit.fInverted:
+ sQuery += '%s AND (%s)\n' % (sExtraIndent, ' OR '.join(asConditions));
+ else:
+ sQuery += '%s AND NOT (%s)\n' % (sExtraIndent, ' OR '.join(asConditions));
+ else:
+ assert len(oCrit.asTables) == 1;
+ sQuery += '%s AND (' % (sExtraIndent,);
+
+ if oCrit.sType != FilterCriterion.ksType_UIntNil or max(oCrit.aoSelected) != -1:
+ if iCrit == self.kiMemory:
+ sQuery += '(%s.%s / 1024)' % (oCrit.asTables[0], oCrit.sColumn,);
+ else:
+ sQuery += '%s.%s' % (oCrit.asTables[0], oCrit.sColumn,);
+ if not oCrit.fInverted:
+ sQuery += ' IN (';
+ else:
+ sQuery += ' NOT IN (';
+ if oCrit.sType == FilterCriterion.ksType_String:
+ sQuery += ', '.join('\'%s\'' % (sValue,) for sValue in oCrit.aoSelected) + ')';
+ else:
+ sQuery += ', '.join(str(iValue) for iValue in oCrit.aoSelected if iValue != -1) + ')';
+
+ if oCrit.sType == FilterCriterion.ksType_UIntNil \
+ and -1 in oCrit.aoSelected:
+ if sQuery[-1] != '(': sQuery += ' OR ';
+ sQuery += '%s.%s IS NULL' % (oCrit.asTables[0], oCrit.sColumn,);
+
+ if iCrit == self.kiFailReasons:
+ if oCrit.fInverted:
+ sQuery += '%s OR TestResultFailures.idFailureReason IS NULL\n' % (sExtraIndent,);
+ else:
+ sQuery += '%s AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n' % (sExtraIndent,);
+ sQuery += ')\n';
+ if oCrit.oSub is not None:
+ sQuery += self._getWhereWorker(iCrit | (((iCrit >> 8) + 1) << 8), oCrit.oSub, sExtraIndent, iOmit);
+ return sQuery;
+
+ def getWhereConditions(self, sExtraIndent = '', iOmit = -1):
+ """
+ Construct the WHERE conditions for the filter, optionally omitting one
+ criterion.
+ """
+ sQuery = '';
+ for iCrit, oCrit in enumerate(self.aCriteria):
+ sQuery += self._getWhereWorker(iCrit, oCrit, sExtraIndent, iOmit);
+ return sQuery;
+
+ def getTableJoins(self, sExtraIndent = '', iOmit = -1, dOmitTables = None):
+ """
+ Construct the WHERE conditions for the filter, optionally omitting one
+ criterion.
+ """
+ afDone = { 'TestSets': True, };
+ if dOmitTables is not None:
+ afDone.update(dOmitTables);
+
+ sQuery = '';
+ for iCrit, oCrit in enumerate(self.aCriteria):
+ if oCrit.sState == FilterCriterion.ksState_Selected \
+ and iCrit != iOmit:
+ for sTable in oCrit.asTables:
+ if sTable not in afDone:
+ afDone[sTable] = True;
+ if sTable == 'Builds':
+ sQuery += '%sINNER JOIN Builds\n' \
+ '%s ON Builds.idBuild = TestSets.idBuild\n' \
+ '%s AND Builds.tsExpire > TestSets.tsCreated\n' \
+ '%s AND Builds.tsEffective <= TestSets.tsCreated\n' \
+ % ( sExtraIndent, sExtraIndent, sExtraIndent, sExtraIndent, );
+ elif sTable == 'BuildCategories':
+ sQuery += '%sINNER JOIN BuildCategories\n' \
+ '%s ON BuildCategories.idBuildCategory = TestSets.idBuildCategory\n' \
+ % ( sExtraIndent, sExtraIndent, );
+ elif sTable == 'TestBoxesWithStrings':
+ sQuery += '%sLEFT OUTER JOIN TestBoxesWithStrings\n' \
+ '%s ON TestBoxesWithStrings.idGenTestBox = TestSets.idGenTestBox\n' \
+ % ( sExtraIndent, sExtraIndent, );
+ elif sTable == 'TestCases':
+ sQuery += '%sINNER JOIN TestCases\n' \
+ '%s ON TestCases.idGenTestCase = TestSets.idGenTestCase\n' \
+ % ( sExtraIndent, sExtraIndent, );
+ elif sTable == 'TestCaseArgs':
+ sQuery += '%sINNER JOIN TestCaseArgs\n' \
+ '%s ON TestCaseArgs.idGenTestCaseArgs = TestSets.idGenTestCaseArgs\n' \
+ % ( sExtraIndent, sExtraIndent, );
+ elif sTable == 'TestResults':
+ sQuery += '%sINNER JOIN TestResults\n' \
+ '%s ON TestResults.idTestResult = TestSets.idTestResult\n' \
+ % ( sExtraIndent, sExtraIndent, );
+ elif sTable == 'TestResultFailures':
+ sQuery += '%sLEFT OUTER JOIN TestResultFailures\n' \
+ '%s ON TestResultFailures.idTestSet = TestSets.idTestSet\n' \
+ '%s AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' \
+ % ( sExtraIndent, sExtraIndent, sExtraIndent, );
+ else:
+ assert False, sTable;
+ return sQuery;
+
+ def isJoiningWithTable(self, sTable):
+ """ Checks whether getTableJoins already joins with TestResultFailures. """
+ for oCrit in self.aCriteria:
+ if oCrit.sState == FilterCriterion.ksState_Selected and sTable in oCrit.asTables:
+ return True;
+ return False
+
+
+
+class TestResultLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ Results grouped by scheduling group.
+ """
+
+ #
+ # Result grinding for displaying in the WUI.
+ #
+
+ ksResultsGroupingTypeNone = 'ResultsGroupingTypeNone';
+ ksResultsGroupingTypeTestGroup = 'ResultsGroupingTypeTestGroup';
+ ksResultsGroupingTypeBuildCat = 'ResultsGroupingTypeBuildCat';
+ ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuildRev';
+ ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox';
+ ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase';
+ ksResultsGroupingTypeOS = 'ResultsGroupingTypeOS';
+ ksResultsGroupingTypeArch = 'ResultsGroupingTypeArch';
+ ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup';
+
+ ## @name Result sorting options.
+ ## @{
+ ksResultsSortByRunningAndStart = 'ResultsSortByRunningAndStart'; ##< Default
+ ksResultsSortByBuildRevision = 'ResultsSortByBuildRevision';
+ ksResultsSortByTestBoxName = 'ResultsSortByTestBoxName';
+ ksResultsSortByTestBoxOs = 'ResultsSortByTestBoxOs';
+ ksResultsSortByTestBoxOsVersion = 'ResultsSortByTestBoxOsVersion';
+ ksResultsSortByTestBoxOsArch = 'ResultsSortByTestBoxOsArch';
+ ksResultsSortByTestBoxArch = 'ResultsSortByTestBoxArch';
+ ksResultsSortByTestBoxCpuVendor = 'ResultsSortByTestBoxCpuVendor';
+ ksResultsSortByTestBoxCpuName = 'ResultsSortByTestBoxCpuName';
+ ksResultsSortByTestBoxCpuRev = 'ResultsSortByTestBoxCpuRev';
+ ksResultsSortByTestBoxCpuFeatures = 'ResultsSortByTestBoxCpuFeatures';
+ ksResultsSortByTestCaseName = 'ResultsSortByTestCaseName';
+ ksResultsSortByFailureReason = 'ResultsSortByFailureReason';
+ kasResultsSortBy = {
+ ksResultsSortByRunningAndStart,
+ ksResultsSortByBuildRevision,
+ ksResultsSortByTestBoxName,
+ ksResultsSortByTestBoxOs,
+ ksResultsSortByTestBoxOsVersion,
+ ksResultsSortByTestBoxOsArch,
+ ksResultsSortByTestBoxArch,
+ ksResultsSortByTestBoxCpuVendor,
+ ksResultsSortByTestBoxCpuName,
+ ksResultsSortByTestBoxCpuRev,
+ ksResultsSortByTestBoxCpuFeatures,
+ ksResultsSortByTestCaseName,
+ ksResultsSortByFailureReason,
+ };
+ ## Used by the WUI for generating the drop down.
+ kaasResultsSortByTitles = (
+ ( ksResultsSortByRunningAndStart, 'Running & Start TS' ),
+ ( ksResultsSortByBuildRevision, 'Build Revision' ),
+ ( ksResultsSortByTestBoxName, 'TestBox Name' ),
+ ( ksResultsSortByTestBoxOs, 'O/S' ),
+ ( ksResultsSortByTestBoxOsVersion, 'O/S Version' ),
+ ( ksResultsSortByTestBoxOsArch, 'O/S & Architecture' ),
+ ( ksResultsSortByTestBoxArch, 'Architecture' ),
+ ( ksResultsSortByTestBoxCpuVendor, 'CPU Vendor' ),
+ ( ksResultsSortByTestBoxCpuName, 'CPU Vendor & Name' ),
+ ( ksResultsSortByTestBoxCpuRev, 'CPU Vendor & Revision' ),
+ ( ksResultsSortByTestBoxCpuFeatures, 'CPU Features' ),
+ ( ksResultsSortByTestCaseName, 'Test Case Name' ),
+ ( ksResultsSortByFailureReason, 'Failure Reason' ),
+ );
+ ## @}
+
+ ## Default sort by map.
+ kdResultSortByMap = {
+ ksResultsSortByRunningAndStart: ( (), None, None, '', '' ),
+ ksResultsSortByBuildRevision: (
+ # Sorting tables.
+ ('Builds',),
+ # Sorting table join(s).
+ ' AND TestSets.idBuild = Builds.idBuild'
+ ' AND Builds.tsExpire >= TestSets.tsCreated'
+ ' AND Builds.tsEffective <= TestSets.tsCreated',
+ # Start of ORDER BY statement.
+ ' Builds.iRevision DESC',
+ # Extra columns to fetch for the above ORDER BY to work in a SELECT DISTINCT statement.
+ '',
+ # Columns for the GROUP BY
+ ''),
+ ksResultsSortByTestBoxName: (
+ ('TestBoxes',),
+ ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+ ' TestBoxes.sName DESC',
+ '', '' ),
+ ksResultsSortByTestBoxOsArch: (
+ ('TestBoxesWithStrings',),
+ ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
+ ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sCpuArch',
+ '', '' ),
+ ksResultsSortByTestBoxOs: (
+ ('TestBoxesWithStrings',),
+ ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
+ ' TestBoxesWithStrings.sOs',
+ '', '' ),
+ ksResultsSortByTestBoxOsVersion: (
+ ('TestBoxesWithStrings',),
+ ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
+ ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sOsVersion DESC',
+ '', '' ),
+ ksResultsSortByTestBoxArch: (
+ ('TestBoxesWithStrings',),
+ ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
+ ' TestBoxesWithStrings.sCpuArch',
+ '', '' ),
+ ksResultsSortByTestBoxCpuVendor: (
+ ('TestBoxesWithStrings',),
+ ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
+ ' TestBoxesWithStrings.sCpuVendor',
+ '', '' ),
+ ksResultsSortByTestBoxCpuName: (
+ ('TestBoxesWithStrings',),
+ ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
+ ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.sCpuName',
+ '', '' ),
+ ksResultsSortByTestBoxCpuRev: (
+ ('TestBoxesWithStrings',),
+ ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
+ ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.lCpuRevision DESC',
+ ', TestBoxesWithStrings.lCpuRevision',
+ ', TestBoxesWithStrings.lCpuRevision' ),
+ ksResultsSortByTestBoxCpuFeatures: (
+ ('TestBoxes',),
+ ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
+ ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, TestBoxes.cCpus DESC',
+ '',
+ '' ),
+ ksResultsSortByTestCaseName: (
+ ('TestCases',),
+ ' AND TestSets.idGenTestCase = TestCases.idGenTestCase',
+ ' TestCases.sName',
+ '', '' ),
+ ksResultsSortByFailureReason: (
+ (), '',
+ 'asSortByFailureReason ASC',
+ ', array_agg(FailureReasons.sShort ORDER BY TestResultFailures.idTestResult) AS asSortByFailureReason',
+ '' ),
+ };
+
+ kdResultGroupingMap = {
+ ksResultsGroupingTypeNone: (
+ # Grouping tables;
+ (),
+ # Grouping field;
+ None,
+ # Grouping where addition.
+ None,
+ # Sort by overrides.
+ {},
+ ),
+ ksResultsGroupingTypeTestGroup: ('', 'TestSets.idTestGroup', None, {},),
+ ksResultsGroupingTypeTestBox: ('', 'TestSets.idTestBox', None, {},),
+ ksResultsGroupingTypeTestCase: ('', 'TestSets.idTestCase', None, {},),
+ ksResultsGroupingTypeOS: (
+ ('TestBoxes',),
+ 'TestBoxes.idStrOs',
+ ' AND TestBoxes.idGenTestBox = TestSets.idGenTestBox',
+ {},
+ ),
+ ksResultsGroupingTypeArch: (
+ ('TestBoxes',),
+ 'TestBoxes.idStrCpuArch',
+ ' AND TestBoxes.idGenTestBox = TestSets.idGenTestBox',
+ {},
+ ),
+ ksResultsGroupingTypeBuildCat: ('', 'TestSets.idBuildCategory', None, {},),
+ ksResultsGroupingTypeBuildRev: (
+ ('Builds',),
+ 'Builds.iRevision',
+ ' AND Builds.idBuild = TestSets.idBuild'
+ ' AND Builds.tsExpire > TestSets.tsCreated'
+ ' AND Builds.tsEffective <= TestSets.tsCreated',
+ { ksResultsSortByBuildRevision: ( (), None, ' Builds.iRevision DESC' ), }
+ ),
+ ksResultsGroupingTypeSchedGroup: ( '', 'TestSets.idSchedGroup', None, {},),
+ };
+
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.oFailureReasonLogic = None;
+ self.oUserAccountLogic = None;
+
+ def _getTimePeriodQueryPart(self, tsNow, sInterval, sExtraIndent = ''):
+ """
+ Get part of SQL query responsible for SELECT data within
+ specified period of time.
+ """
+ assert sInterval is not None; # too many rows.
+
+ cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.)
+ if tsNow is None:
+ sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \
+ '%s AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \
+ % ( sInterval,
+ sExtraIndent, sInterval, cMonthsMourningPeriod);
+ else:
+ sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh.
+ sRet = 'TestSets.tsCreated <= %s\n' \
+ '%s AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \
+ '%s AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \
+ % ( sTsNow,
+ sExtraIndent, sTsNow, sInterval, cMonthsMourningPeriod,
+ sExtraIndent, sTsNow, sInterval );
+ return sRet
+
+ def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, oFilter, enmResultSortBy, # pylint: disable=too-many-arguments
+ enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):
+ """
+ Fetches TestResults table content.
+
+ If @param enmResultsGroupingType and @param iResultsGroupingValue
+ are not None, then resulting (returned) list contains only records
+ that match specified @param enmResultsGroupingType.
+
+ If @param enmResultsGroupingType is None, then
+ @param iResultsGroupingValue is ignored.
+
+ Returns an array (list) of TestResultData items, empty list if none.
+ Raises exception on error.
+ """
+
+ _ = oFilter;
+
+ #
+ # Get SQL query parameters
+ #
+ if enmResultsGroupingType is None or enmResultsGroupingType not in self.kdResultGroupingMap:
+ raise TMExceptionBase('Unknown grouping type');
+ if enmResultSortBy is None or enmResultSortBy not in self.kasResultsSortBy:
+ raise TMExceptionBase('Unknown sorting');
+ asGroupingTables, sGroupingField, sGroupingCondition, dSortOverrides = self.kdResultGroupingMap[enmResultsGroupingType];
+ if enmResultSortBy in dSortOverrides:
+ asSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = dSortOverrides[enmResultSortBy];
+ else:
+ asSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = self.kdResultSortByMap[enmResultSortBy];
+
+ #
+ # Construct the query.
+ #
+ sQuery = 'SELECT DISTINCT TestSets.idTestSet,\n' \
+ ' BuildCategories.idBuildCategory,\n' \
+ ' BuildCategories.sProduct,\n' \
+ ' BuildCategories.sRepository,\n' \
+ ' BuildCategories.sBranch,\n' \
+ ' BuildCategories.sType,\n' \
+ ' Builds.idBuild,\n' \
+ ' Builds.sVersion,\n' \
+ ' Builds.iRevision,\n' \
+ ' TestBoxesWithStrings.sOs,\n' \
+ ' TestBoxesWithStrings.sOsVersion,\n' \
+ ' TestBoxesWithStrings.sCpuArch,\n' \
+ ' TestBoxesWithStrings.sCpuVendor,\n' \
+ ' TestBoxesWithStrings.sCpuName,\n' \
+ ' TestBoxesWithStrings.cCpus,\n' \
+ ' TestBoxesWithStrings.fCpuHwVirt,\n' \
+ ' TestBoxesWithStrings.fCpuNestedPaging,\n' \
+ ' TestBoxesWithStrings.fCpu64BitGuest,\n' \
+ ' TestBoxesWithStrings.idTestBox,\n' \
+ ' TestBoxesWithStrings.sName,\n' \
+ ' TestResults.tsCreated,\n' \
+ ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated) AS tsElapsedTestResult,\n' \
+ ' TestSets.enmStatus,\n' \
+ ' TestResults.cErrors,\n' \
+ ' TestCases.idTestCase,\n' \
+ ' TestCases.sName,\n' \
+ ' TestCases.sBaseCmd,\n' \
+ ' TestCaseArgs.sArgs,\n' \
+ ' TestCaseArgs.sSubName,\n' \
+ ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
+ ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
+ ' array_agg(TestResultFailures.idFailureReason ORDER BY TestResultFailures.idTestResult),\n' \
+ ' array_agg(TestResultFailures.uidAuthor ORDER BY TestResultFailures.idTestResult),\n' \
+ ' array_agg(TestResultFailures.tsEffective ORDER BY TestResultFailures.idTestResult),\n' \
+ ' array_agg(TestResultFailures.sComment ORDER BY TestResultFailures.idTestResult),\n' \
+ ' (TestSets.tsDone IS NULL) SortRunningFirst' + sSortColumns + '\n' \
+ 'FROM ( SELECT TestSets.idTestSet AS idTestSet,\n' \
+ ' TestSets.tsDone AS tsDone,\n' \
+ ' TestSets.tsCreated AS tsCreated,\n' \
+ ' TestSets.enmStatus AS enmStatus,\n' \
+ ' TestSets.idBuild AS idBuild,\n' \
+ ' TestSets.idBuildTestSuite AS idBuildTestSuite,\n' \
+ ' TestSets.idGenTestBox AS idGenTestBox,\n' \
+ ' TestSets.idGenTestCase AS idGenTestCase,\n' \
+ ' TestSets.idGenTestCaseArgs AS idGenTestCaseArgs\n' \
+ ' FROM TestSets\n';
+ sQuery += oFilter.getTableJoins(' ');
+ if fOnlyNeedingReason and not oFilter.isJoiningWithTable('TestResultFailures'):
+ sQuery += '\n' \
+ ' LEFT OUTER JOIN TestResultFailures\n' \
+ ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
+ ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
+ for asTables in [asGroupingTables, asSortTables]:
+ for sTable in asTables:
+ if not oFilter.isJoiningWithTable(sTable):
+ sQuery = sQuery[:-1] + ',\n ' + sTable + '\n';
+
+ sQuery += ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval, ' ') + \
+ oFilter.getWhereConditions(' ');
+ if fOnlyFailures or fOnlyNeedingReason:
+ sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
+ ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
+ if fOnlyNeedingReason:
+ sQuery += ' AND TestResultFailures.idTestSet IS NULL\n';
+ if sGroupingField is not None:
+ sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);
+ if sGroupingCondition is not None:
+ sQuery += sGroupingCondition.replace(' AND ', ' AND ');
+ if sSortWhere is not None:
+ sQuery += sSortWhere.replace(' AND ', ' AND ');
+ sQuery += ' ORDER BY ';
+ if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') < 0:
+ sQuery += sSortOrderBy + ',\n ';
+ sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \
+ ' LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,);
+
+ # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
+ # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
+ sQuery += ' ) AS TestSets\n' \
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n' \
+ ' ON TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox' \
+ ' LEFT OUTER JOIN Builds AS TestSuiteBits\n' \
+ ' ON TestSuiteBits.idBuild = TestSets.idBuildTestSuite\n' \
+ ' AND TestSuiteBits.tsExpire > TestSets.tsCreated\n' \
+ ' AND TestSuiteBits.tsEffective <= TestSets.tsCreated\n' \
+ ' LEFT OUTER JOIN TestResultFailures\n' \
+ ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
+ ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
+ if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') >= 0:
+ sQuery += '\n' \
+ ' LEFT OUTER JOIN FailureReasons\n' \
+ ' ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \
+ ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP';
+ sQuery += ',\n' \
+ ' BuildCategories,\n' \
+ ' Builds,\n' \
+ ' TestResults,\n' \
+ ' TestCases,\n' \
+ ' TestCaseArgs\n';
+ sQuery += 'WHERE TestSets.idTestSet = TestResults.idTestSet\n' \
+ ' AND TestResults.idTestResultParent is NULL\n' \
+ ' AND TestSets.idBuild = Builds.idBuild\n' \
+ ' AND Builds.tsExpire > TestSets.tsCreated\n' \
+ ' AND Builds.tsEffective <= TestSets.tsCreated\n' \
+ ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \
+ ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \
+ ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n';
+ sQuery += 'GROUP BY TestSets.idTestSet,\n' \
+ ' BuildCategories.idBuildCategory,\n' \
+ ' BuildCategories.sProduct,\n' \
+ ' BuildCategories.sRepository,\n' \
+ ' BuildCategories.sBranch,\n' \
+ ' BuildCategories.sType,\n' \
+ ' Builds.idBuild,\n' \
+ ' Builds.sVersion,\n' \
+ ' Builds.iRevision,\n' \
+ ' TestBoxesWithStrings.sOs,\n' \
+ ' TestBoxesWithStrings.sOsVersion,\n' \
+ ' TestBoxesWithStrings.sCpuArch,\n' \
+ ' TestBoxesWithStrings.sCpuVendor,\n' \
+ ' TestBoxesWithStrings.sCpuName,\n' \
+ ' TestBoxesWithStrings.cCpus,\n' \
+ ' TestBoxesWithStrings.fCpuHwVirt,\n' \
+ ' TestBoxesWithStrings.fCpuNestedPaging,\n' \
+ ' TestBoxesWithStrings.fCpu64BitGuest,\n' \
+ ' TestBoxesWithStrings.idTestBox,\n' \
+ ' TestBoxesWithStrings.sName,\n' \
+ ' TestResults.tsCreated,\n' \
+ ' tsElapsedTestResult,\n' \
+ ' TestSets.enmStatus,\n' \
+ ' TestResults.cErrors,\n' \
+ ' TestCases.idTestCase,\n' \
+ ' TestCases.sName,\n' \
+ ' TestCases.sBaseCmd,\n' \
+ ' TestCaseArgs.sArgs,\n' \
+ ' TestCaseArgs.sSubName,\n' \
+ ' TestSuiteBits.idBuild,\n' \
+ ' TestSuiteBits.iRevision,\n' \
+ ' SortRunningFirst' + sSortGroupBy + '\n';
+ sQuery += 'ORDER BY ';
+ if sSortOrderBy is not None:
+ sQuery += sSortOrderBy.replace('TestBoxes.', 'TestBoxesWithStrings.') + ',\n ';
+ sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n';
+
+ #
+ # Execute the query and return the wrapped results.
+ #
+ self._oDb.execute(sQuery);
+
+ if self.oFailureReasonLogic is None:
+ self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
+ if self.oUserAccountLogic is None:
+ self.oUserAccountLogic = UserAccountLogic(self._oDb);
+
+ aoRows = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRows.append(TestResultListingData().initFromDbRowEx(aoRow, self.oFailureReasonLogic, self.oUserAccountLogic));
+
+ return aoRows
+
+
+ def fetchTimestampsForLogViewer(self, idTestSet):
+ """
+ Returns an ordered list with all the test result timestamps, both start
+ and end.
+
+ The log viewer create anchors in the log text so we can jump directly to
+ the log lines relevant for a test event.
+ """
+ self._oDb.execute('(\n'
+ 'SELECT tsCreated\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestSet = %s\n'
+ ') UNION (\n'
+ 'SELECT tsCreated + tsElapsed\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND tsElapsed IS NOT NULL\n'
+ ') UNION (\n'
+ 'SELECT TestResultFiles.tsCreated\n'
+ 'FROM TestResultFiles\n'
+ 'WHERE idTestSet = %s\n'
+ ') UNION (\n'
+ 'SELECT tsCreated\n'
+ 'FROM TestResultValues\n'
+ 'WHERE idTestSet = %s\n'
+ ') UNION (\n'
+ 'SELECT TestResultMsgs.tsCreated\n'
+ 'FROM TestResultMsgs\n'
+ 'WHERE idTestSet = %s\n'
+ ') ORDER by 1'
+ , ( idTestSet, idTestSet, idTestSet, idTestSet, idTestSet, ));
+ return [aoRow[0] for aoRow in self._oDb.fetchAll()];
+
+
+ def getEntriesCount(self, tsNow, sInterval, oFilter, enmResultsGroupingType, iResultsGroupingValue,
+ fOnlyFailures, fOnlyNeedingReason):
+ """
+ Get number of table records.
+
+ If @param enmResultsGroupingType and @param iResultsGroupingValue
+ are not None, then we count only only those records
+ that match specified @param enmResultsGroupingType.
+
+ If @param enmResultsGroupingType is None, then
+ @param iResultsGroupingValue is ignored.
+ """
+ _ = oFilter;
+
+ #
+ # Get SQL query parameters
+ #
+ if enmResultsGroupingType is None:
+ raise TMExceptionBase('Unknown grouping type')
+
+ if enmResultsGroupingType not in self.kdResultGroupingMap:
+ raise TMExceptionBase('Unknown grouping type')
+ asGroupingTables, sGroupingField, sGroupingCondition, _ = self.kdResultGroupingMap[enmResultsGroupingType];
+
+ #
+ # Construct the query.
+ #
+ sQuery = 'SELECT COUNT(TestSets.idTestSet)\n' \
+ 'FROM TestSets\n';
+ sQuery += oFilter.getTableJoins();
+ if fOnlyNeedingReason and not oFilter.isJoiningWithTable('TestResultFailures'):
+ sQuery += ' LEFT OUTER JOIN TestResultFailures\n' \
+ ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
+ ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n';
+ for sTable in asGroupingTables:
+ if not oFilter.isJoiningWithTable(sTable):
+ sQuery = sQuery[:-1] + ',\n ' + sTable + '\n';
+ sQuery += 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval) + \
+ oFilter.getWhereConditions();
+ if fOnlyFailures or fOnlyNeedingReason:
+ sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
+ ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
+ if fOnlyNeedingReason:
+ sQuery += ' AND TestResultFailures.idTestSet IS NULL\n';
+ if sGroupingField is not None:
+ sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);
+ if sGroupingCondition is not None:
+ sQuery += sGroupingCondition.replace(' AND ', ' AND ');
+
+ #
+ # Execute the query and return the result.
+ #
+ self._oDb.execute(sQuery)
+ return self._oDb.fetchOne()[0]
+
+ def getTestGroups(self, tsNow, sPeriod):
+ """
+ Get list of uniq TestGroupData objects which
+ found in all test results.
+ """
+
+ self._oDb.execute('SELECT DISTINCT TestGroups.*\n'
+ 'FROM TestGroups, TestSets\n'
+ 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n'
+ ' AND TestGroups.tsExpire > TestSets.tsCreated\n'
+ ' AND TestGroups.tsEffective <= TestSets.tsCreated'
+ ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(TestGroupData().initFromDbRow(aoRow))
+ return aoRet
+
+ def getBuilds(self, tsNow, sPeriod):
+ """
+ Get list of uniq BuildDataEx objects which
+ found in all test results.
+ """
+
+ self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n'
+ 'FROM Builds, BuildCategories, TestSets\n'
+ 'WHERE TestSets.idBuild = Builds.idBuild\n'
+ ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
+ ' AND Builds.tsExpire > TestSets.tsCreated\n'
+ ' AND Builds.tsEffective <= TestSets.tsCreated'
+ ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
+ aaoRows = self._oDb.fetchAll()
+ aoRet = []
+ for aoRow in aaoRows:
+ aoRet.append(BuildDataEx().initFromDbRow(aoRow))
+ return aoRet
+
+ def getTestBoxes(self, tsNow, sPeriod):
+ """
+ Get list of uniq TestBoxData objects which
+ found in all test results.
+ """
+ # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
+ # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
+ self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
+ 'FROM ( SELECT idTestBox AS idTestBox,\n'
+ ' MAX(idGenTestBox) AS idGenTestBox\n'
+ ' FROM TestSets\n'
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ ' GROUP BY idTestBox\n'
+ ' ) AS TestBoxIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
+ 'ORDER BY TestBoxesWithStrings.sName\n' );
+ aoRet = []
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(TestBoxData().initFromDbRow(aoRow));
+ return aoRet
+
+ def getTestCases(self, tsNow, sPeriod):
+ """
+ Get a list of unique TestCaseData objects which is appears in the test
+ specified result period.
+ """
+
+ # Using LEFT OUTER JOIN instead of INNER JOIN in case it performs better, doesn't matter for the result.
+ self._oDb.execute('SELECT TestCases.*\n'
+ 'FROM ( SELECT idTestCase AS idTestCase,\n'
+ ' MAX(idGenTestCase) AS idGenTestCase\n'
+ ' FROM TestSets\n'
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ ' GROUP BY idTestCase\n'
+ ' ) AS TestCasesIDs\n'
+ ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCasesIDs.idGenTestCase\n'
+ 'ORDER BY TestCases.sName\n' );
+
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(TestCaseData().initFromDbRow(aoRow));
+ return aoRet
+
+ def getOSes(self, tsNow, sPeriod):
+ """
+ Get a list of [idStrOs, sOs] tuples of the OSes that appears in the specified result period.
+ """
+
+ # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
+ # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
+ self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrOs, TestBoxesWithStrings.sOs\n'
+ 'FROM ( SELECT idTestBox AS idTestBox,\n'
+ ' MAX(idGenTestBox) AS idGenTestBox\n'
+ ' FROM TestSets\n'
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ ' GROUP BY idTestBox\n'
+ ' ) AS TestBoxIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
+ 'ORDER BY TestBoxesWithStrings.sOs\n' );
+ return self._oDb.fetchAll();
+
+ def getArchitectures(self, tsNow, sPeriod):
+ """
+ Get a list of [idStrCpuArch, sCpuArch] tuples of the architecutres
+ that appears in the specified result period.
+ """
+
+ # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
+ # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
+ self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrCpuArch, TestBoxesWithStrings.sCpuArch\n'
+ 'FROM ( SELECT idTestBox AS idTestBox,\n'
+ ' MAX(idGenTestBox) AS idGenTestBox\n'
+ ' FROM TestSets\n'
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ ' GROUP BY idTestBox\n'
+ ' ) AS TestBoxIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
+ 'ORDER BY TestBoxesWithStrings.sCpuArch\n' );
+ return self._oDb.fetchAll();
+
+ def getBuildCategories(self, tsNow, sPeriod):
+ """
+ Get a list of BuildCategoryData that appears in the specified result period.
+ """
+
+ self._oDb.execute('SELECT DISTINCT BuildCategories.*\n'
+ 'FROM ( SELECT DISTINCT idBuildCategory AS idBuildCategory\n'
+ ' FROM TestSets\n'
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ ' ) AS BuildCategoryIDs\n'
+ ' LEFT OUTER JOIN BuildCategories\n'
+ ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n'
+ 'ORDER BY BuildCategories.sProduct, BuildCategories.sBranch, BuildCategories.sType\n');
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(BuildCategoryData().initFromDbRow(aoRow));
+ return aoRet;
+
+ def getSchedGroups(self, tsNow, sPeriod):
+ """
+ Get list of uniq SchedGroupData objects which
+ found in all test results.
+ """
+
+ self._oDb.execute('SELECT SchedGroups.*\n'
+ 'FROM ( SELECT idSchedGroup,\n'
+ ' MAX(TestSets.tsCreated) AS tsNow\n'
+ ' FROM TestSets\n'
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ ' GROUP BY idSchedGroup\n'
+ ' ) AS SchedGroupIDs\n'
+ ' INNER JOIN SchedGroups\n'
+ ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n'
+ ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n'
+ ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n'
+ 'ORDER BY SchedGroups.sName\n' );
+ aoRet = []
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(SchedGroupData().initFromDbRow(aoRow));
+ return aoRet
+
+ def getById(self, idTestResult):
+ """
+ Get build record by its id
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestResult = %s\n',
+ (idTestResult,))
+
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise TMTooManyRows('Found more than one test result with the same credentials. Database structure is corrupted.')
+ try:
+ return TestResultData().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+ def fetchPossibleFilterOptions(self, oFilter, tsNow, sPeriod, oReportModel = None):
+ """
+ Fetches the available filter criteria, given the current filtering.
+
+ Returns oFilter.
+ """
+ assert isinstance(oFilter, TestResultFilter);
+
+ # Hack to avoid lot's of conditionals or duplicate this code.
+ if oReportModel is None:
+ class DummyReportModel(object):
+ """ Dummy """
+ def getExtraSubjectTables(self):
+ """ Dummy """
+ return [];
+ def getExtraSubjectWhereExpr(self):
+ """ Dummy """
+ return '';
+ oReportModel = DummyReportModel();
+
+ def workerDoFetch(oMissingLogicType, sNameAttr = 'sName', fIdIsName = False, idxHover = -1,
+ idNull = -1, sNullDesc = '<NULL>'):
+ """ Does the tedious result fetching and handling of missing bits. """
+ dLeft = { oValue: 1 for oValue in oCrit.aoSelected };
+ oCrit.aoPossible = [];
+ for aoRow in self._oDb.fetchAll():
+ oCrit.aoPossible.append(FilterCriterionValueAndDescription(aoRow[0] if aoRow[0] is not None else idNull,
+ aoRow[1] if aoRow[1] is not None else sNullDesc,
+ aoRow[2],
+ aoRow[idxHover] if idxHover >= 0 else None));
+ if aoRow[0] in dLeft:
+ del dLeft[aoRow[0]];
+ if dLeft:
+ if fIdIsName:
+ for idMissing in dLeft:
+ oCrit.aoPossible.append(FilterCriterionValueAndDescription(idMissing, str(idMissing),
+ fIrrelevant = True));
+ else:
+ oMissingLogic = oMissingLogicType(self._oDb);
+ for idMissing in dLeft:
+ oMissing = oMissingLogic.cachedLookup(idMissing);
+ if oMissing is not None:
+ oCrit.aoPossible.append(FilterCriterionValueAndDescription(idMissing,
+ getattr(oMissing, sNameAttr),
+ fIrrelevant = True));
+
+ def workerDoFetchNested():
+ """ Does the tedious result fetching and handling of missing bits. """
+ oCrit.aoPossible = [];
+ oCrit.oSub.aoPossible = [];
+ dLeft = { oValue: 1 for oValue in oCrit.aoSelected };
+ dSubLeft = { oValue: 1 for oValue in oCrit.oSub.aoSelected };
+ oMain = None;
+ for aoRow in self._oDb.fetchAll():
+ if oMain is None or oMain.oValue != aoRow[0]:
+ oMain = FilterCriterionValueAndDescription(aoRow[0], aoRow[1], 0);
+ oCrit.aoPossible.append(oMain);
+ if aoRow[0] in dLeft:
+ del dLeft[aoRow[0]];
+ oCurSub = FilterCriterionValueAndDescription(aoRow[2], aoRow[3], aoRow[4]);
+ oCrit.oSub.aoPossible.append(oCurSub);
+ if aoRow[2] in dSubLeft:
+ del dSubLeft[aoRow[2]];
+
+ oMain.aoSubs.append(oCurSub);
+ oMain.cTimes += aoRow[4];
+
+ if dLeft:
+ pass; ## @todo
+
+ # Statuses.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiTestStatus];
+ self._oDb.execute('SELECT TestSets.enmStatus, TestSets.enmStatus, COUNT(TestSets.idTestSet)\n'
+ 'FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestStatus) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestStatus) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ 'GROUP BY TestSets.enmStatus\n'
+ 'ORDER BY TestSets.enmStatus\n');
+ workerDoFetch(None, fIdIsName = True);
+
+ # Scheduling groups (see getSchedGroups).
+ oCrit = oFilter.aCriteria[TestResultFilter.kiSchedGroups];
+ self._oDb.execute('SELECT SchedGroups.idSchedGroup, SchedGroups.sName, SchedGroupIDs.cTimes\n'
+ 'FROM ( SELECT TestSets.idSchedGroup,\n'
+ ' MAX(TestSets.tsCreated) AS tsNow,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiSchedGroups) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiSchedGroups) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idSchedGroup\n'
+ ' ) AS SchedGroupIDs\n'
+ ' INNER JOIN SchedGroups\n'
+ ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n'
+ ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n'
+ ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n'
+ 'ORDER BY SchedGroups.sName\n' );
+ workerDoFetch(SchedGroupLogic);
+
+ # Testboxes (see getTestBoxes).
+ oCrit = oFilter.aCriteria[TestResultFilter.kiTestBoxes];
+ self._oDb.execute('SELECT TestBoxesWithStrings.idTestBox,\n'
+ ' TestBoxesWithStrings.sName,\n'
+ ' TestBoxIDs.cTimes\n'
+ 'FROM ( SELECT TestSets.idTestBox AS idTestBox,\n'
+ ' MAX(TestSets.idGenTestBox) AS idGenTestBox,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestBoxes) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestBoxes) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idTestBox\n'
+ ' ) AS TestBoxIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
+ 'ORDER BY TestBoxesWithStrings.sName\n' );
+ workerDoFetch(TestBoxLogic);
+
+ # Testbox OSes and versions.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiOses];
+ self._oDb.execute('SELECT TestBoxesWithStrings.idStrOs,\n'
+ ' TestBoxesWithStrings.sOs,\n'
+ ' TestBoxesWithStrings.idStrOsVersion,\n'
+ ' TestBoxesWithStrings.sOsVersion,\n'
+ ' SUM(TestBoxGenIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idGenTestBox,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiOses) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiOses) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idGenTestBox\n'
+ ' ) AS TestBoxGenIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
+ 'GROUP BY TestBoxesWithStrings.idStrOs,\n'
+ ' TestBoxesWithStrings.sOs,\n'
+ ' TestBoxesWithStrings.idStrOsVersion,\n'
+ ' TestBoxesWithStrings.sOsVersion\n'
+ 'ORDER BY TestBoxesWithStrings.sOs,\n'
+ ' TestBoxesWithStrings.sOs = \'win\' AND TestBoxesWithStrings.sOsVersion = \'10\' DESC,\n'
+ ' TestBoxesWithStrings.sOsVersion DESC\n'
+ );
+ workerDoFetchNested();
+
+ # Testbox CPU(/OS) architectures.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiCpuArches];
+ self._oDb.execute('SELECT TestBoxesWithStrings.idStrCpuArch,\n'
+ ' TestBoxesWithStrings.sCpuArch,\n'
+ ' SUM(TestBoxGenIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idGenTestBox,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuArches) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuArches) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idGenTestBox\n'
+ ' ) AS TestBoxGenIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
+ 'GROUP BY TestBoxesWithStrings.idStrCpuArch, TestBoxesWithStrings.sCpuArch\n'
+ 'ORDER BY TestBoxesWithStrings.sCpuArch\n' );
+ workerDoFetch(None, fIdIsName = True);
+
+ # Testbox CPU revisions.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiCpuVendors];
+ self._oDb.execute('SELECT TestBoxesWithStrings.idStrCpuVendor,\n'
+ ' TestBoxesWithStrings.sCpuVendor,\n'
+ ' TestBoxesWithStrings.lCpuRevision,\n'
+ ' TestBoxesWithStrings.sCpuVendor,\n'
+ ' SUM(TestBoxGenIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idGenTestBox,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuVendors) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuVendors) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idGenTestBox'
+ ' ) AS TestBoxGenIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
+ 'GROUP BY TestBoxesWithStrings.idStrCpuVendor,\n'
+ ' TestBoxesWithStrings.sCpuVendor,\n'
+ ' TestBoxesWithStrings.lCpuRevision,\n'
+ ' TestBoxesWithStrings.sCpuVendor\n'
+ 'ORDER BY TestBoxesWithStrings.sCpuVendor DESC,\n'
+ ' TestBoxesWithStrings.sCpuVendor = \'GenuineIntel\'\n'
+ ' AND (TestBoxesWithStrings.lCpuRevision >> 24) = 15,\n' # P4 at the bottom is a start...
+ ' TestBoxesWithStrings.lCpuRevision DESC\n'
+ );
+ workerDoFetchNested();
+ for oCur in oCrit.oSub.aoPossible:
+ oCur.sDesc = TestBoxData.getPrettyCpuVersionEx(oCur.oValue, oCur.sDesc).replace('_', ' ');
+
+ # Testbox CPU core/thread counts.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiCpuCounts];
+ self._oDb.execute('SELECT TestBoxesWithStrings.cCpus,\n'
+ ' CAST(TestBoxesWithStrings.cCpus AS TEXT),\n'
+ ' SUM(TestBoxGenIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idGenTestBox,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuCounts) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuCounts) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idGenTestBox'
+ ' ) AS TestBoxGenIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
+ 'GROUP BY TestBoxesWithStrings.cCpus\n'
+ 'ORDER BY TestBoxesWithStrings.cCpus\n' );
+ workerDoFetch(None, fIdIsName = True);
+
+ # Testbox memory.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiMemory];
+ self._oDb.execute('SELECT TestBoxesWithStrings.cMbMemory / 1024,\n'
+ ' NULL,\n'
+ ' SUM(TestBoxGenIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idGenTestBox,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiMemory) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiMemory) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idGenTestBox'
+ ' ) AS TestBoxGenIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
+ 'GROUP BY TestBoxesWithStrings.cMbMemory / 1024\n'
+ 'ORDER BY 1\n' );
+ workerDoFetch(None, fIdIsName = True);
+ for oCur in oCrit.aoPossible:
+ oCur.sDesc = '%u GB' % (oCur.oValue,);
+
+ # Testbox python versions .
+ oCrit = oFilter.aCriteria[TestResultFilter.kiPythonVersions];
+ self._oDb.execute('SELECT TestBoxesWithStrings.iPythonHexVersion,\n'
+ ' NULL,\n'
+ ' SUM(TestBoxGenIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idGenTestBox AS idGenTestBox,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiPythonVersions) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiPythonVersions) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idGenTestBox\n'
+ ' ) AS TestBoxGenIDs\n'
+ ' LEFT OUTER JOIN TestBoxesWithStrings\n'
+ ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
+ 'GROUP BY TestBoxesWithStrings.iPythonHexVersion\n'
+ 'ORDER BY TestBoxesWithStrings.iPythonHexVersion\n' );
+ workerDoFetch(None, fIdIsName = True);
+ for oCur in oCrit.aoPossible:
+ oCur.sDesc = TestBoxData.formatPythonVersionEx(oCur.oValue); # pylint: disable=redefined-variable-type
+
+ # Testcase with variation.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiTestCases];
+ self._oDb.execute('SELECT TestCaseArgsIDs.idTestCase,\n'
+ ' TestCases.sName,\n'
+ ' TestCaseArgsIDs.idTestCaseArgs,\n'
+ ' CASE WHEN TestCaseArgs.sSubName IS NULL OR TestCaseArgs.sSubName = \'\' THEN\n'
+ ' CONCAT(\'/ #\', TestCaseArgs.idTestCaseArgs)\n'
+ ' ELSE\n'
+ ' TestCaseArgs.sSubName\n'
+ ' END,'
+ ' TestCaseArgsIDs.cTimes\n'
+ 'FROM ( SELECT TestSets.idTestCase AS idTestCase,\n'
+ ' TestSets.idTestCaseArgs AS idTestCaseArgs,\n'
+ ' MAX(TestSets.idGenTestCase) AS idGenTestCase,\n'
+ ' MAX(TestSets.idGenTestCaseArgs) AS idGenTestCaseArgs,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestCases) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestCases) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idTestCase, TestSets.idTestCaseArgs\n'
+ ' ) AS TestCaseArgsIDs\n'
+ ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCaseArgsIDs.idGenTestCase\n'
+ ' LEFT OUTER JOIN TestCaseArgs\n'
+ ' ON TestCaseArgs.idGenTestCaseArgs = TestCaseArgsIDs.idGenTestCaseArgs\n'
+ 'ORDER BY TestCases.sName, 4\n' );
+ workerDoFetchNested();
+
+ # Build revisions.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiRevisions];
+ self._oDb.execute('SELECT Builds.iRevision, CONCAT(\'r\', Builds.iRevision), SUM(BuildIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idBuild AS idBuild,\n'
+ ' MAX(TestSets.tsCreated) AS tsNow,\n'
+ ' COUNT(TestSets.idBuild) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiRevisions) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiRevisions) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idBuild\n'
+ ' ) AS BuildIDs\n'
+ ' INNER JOIN Builds\n'
+ ' ON Builds.idBuild = BuildIDs.idBuild\n'
+ ' AND Builds.tsExpire > BuildIDs.tsNow\n'
+ ' AND Builds.tsEffective <= BuildIDs.tsNow\n'
+ 'GROUP BY Builds.iRevision\n'
+ 'ORDER BY Builds.iRevision DESC\n' );
+ workerDoFetch(None, fIdIsName = True);
+
+ # Build branches.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiBranches];
+ self._oDb.execute('SELECT BuildCategories.sBranch, BuildCategories.sBranch, SUM(BuildCategoryIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idBuildCategory,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiBranches) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiBranches) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idBuildCategory\n'
+ ' ) AS BuildCategoryIDs\n'
+ ' INNER JOIN BuildCategories\n'
+ ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n'
+ 'GROUP BY BuildCategories.sBranch\n'
+ 'ORDER BY BuildCategories.sBranch DESC\n' );
+ workerDoFetch(None, fIdIsName = True);
+
+ # Build types.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiBuildTypes];
+ self._oDb.execute('SELECT BuildCategories.sType, BuildCategories.sType, SUM(BuildCategoryIDs.cTimes)\n'
+ 'FROM ( SELECT TestSets.idBuildCategory,\n'
+ ' COUNT(TestSets.idTestSet) AS cTimes\n'
+ ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiBuildTypes) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiBuildTypes) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestSets.idBuildCategory\n'
+ ' ) AS BuildCategoryIDs\n'
+ ' INNER JOIN BuildCategories\n'
+ ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n'
+ 'GROUP BY BuildCategories.sType\n'
+ 'ORDER BY BuildCategories.sType DESC\n' );
+ workerDoFetch(None, fIdIsName = True);
+
+ # Failure reasons.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiFailReasons];
+ self._oDb.execute('SELECT FailureReasons.idFailureReason, FailureReasons.sShort, FailureReasonIDs.cTimes\n'
+ 'FROM ( SELECT TestResultFailures.idFailureReason,\n'
+ ' COUNT(TestSets.idTestSet) as cTimes\n'
+ ' FROM TestSets\n'
+ ' LEFT OUTER JOIN TestResultFailures\n'
+ ' ON TestResultFailures.idTestSet = TestSets.idTestSet\n'
+ ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' +
+ oFilter.getTableJoins(iOmit = TestResultFilter.kiFailReasons) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ ' AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n' +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiFailReasons) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' GROUP BY TestResultFailures.idFailureReason\n'
+ ' ) AS FailureReasonIDs\n'
+ ' LEFT OUTER JOIN FailureReasons\n'
+ ' ON FailureReasons.idFailureReason = FailureReasonIDs.idFailureReason\n'
+ ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY FailureReasons.idFailureReason IS NULL DESC,\n'
+ ' FailureReasons.sShort\n' );
+ workerDoFetch(FailureReasonLogic, 'sShort', sNullDesc = 'Not given');
+
+ # Error counts.
+ oCrit = oFilter.aCriteria[TestResultFilter.kiErrorCounts];
+ self._oDb.execute('SELECT TestResults.cErrors, CAST(TestResults.cErrors AS TEXT), COUNT(TestResults.idTestResult)\n'
+ 'FROM ( SELECT TestSets.idTestResult AS idTestResult\n'
+ ' FROM TestSets\n' +
+ oFilter.getTableJoins(iOmit = TestResultFilter.kiFailReasons) +
+ ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
+ ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
+ oFilter.getWhereConditions(iOmit = TestResultFilter.kiFailReasons) +
+ oReportModel.getExtraSubjectWhereExpr() +
+ ' ) AS TestSetIDs\n'
+ ' INNER JOIN TestResults\n'
+ ' ON TestResults.idTestResult = TestSetIDs.idTestResult\n'
+ 'GROUP BY TestResults.cErrors\n'
+ 'ORDER BY TestResults.cErrors\n');
+
+ workerDoFetch(None, fIdIsName = True);
+
+ return oFilter;
+
+
+ #
+ # Details view and interface.
+ #
+
+ def fetchResultTree(self, idTestSet, cMaxDepth = None):
+ """
+ Fetches the result tree for the given test set.
+
+ Returns a tree of TestResultDataEx nodes.
+ Raises exception on invalid input and database issues.
+ """
+ # Depth first, i.e. just like the XML added them.
+ ## @todo this still isn't performing extremely well, consider optimizations.
+ sQuery = self._oDb.formatBindArgs(
+ 'SELECT TestResults.*,\n'
+ ' TestResultStrTab.sValue,\n'
+ ' EXISTS ( SELECT idTestResultValue\n'
+ ' FROM TestResultValues\n'
+ ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n'
+ ' EXISTS ( SELECT idTestResultMsg\n'
+ ' FROM TestResultMsgs\n'
+ ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n'
+ ' EXISTS ( SELECT idTestResultFile\n'
+ ' FROM TestResultFiles\n'
+ ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles,\n'
+ ' EXISTS ( SELECT idTestResult\n'
+ ' FROM TestResultFailures\n'
+ ' WHERE TestResultFailures.idTestResult = TestResults.idTestResult ) AS fHasReasons\n'
+ 'FROM TestResults, TestResultStrTab\n'
+ 'WHERE TestResults.idTestSet = %s\n'
+ ' AND TestResults.idStrName = TestResultStrTab.idStr\n'
+ , ( idTestSet, ));
+ if cMaxDepth is not None:
+ sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,));
+ sQuery += 'ORDER BY idTestResult ASC\n'
+
+ self._oDb.execute(sQuery);
+ cRows = self._oDb.getRowCount();
+ if cRows > 65536:
+ raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,));
+
+ aaoRows = self._oDb.fetchAll();
+ if not aaoRows:
+ raise TMRowNotFound('No test results for idTestSet=%d.' % (idTestSet,));
+
+ # Set up the root node first.
+ aoRow = aaoRows[0];
+ oRoot = TestResultDataEx().initFromDbRow(aoRow);
+ if oRoot.idTestResultParent is not None:
+ raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'
+ % (oRoot.idTestResult, oRoot.idTestResultParent));
+ self._fetchResultTreeNodeExtras(oRoot, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
+
+ # The children (if any).
+ dLookup = { oRoot.idTestResult: oRoot };
+ oParent = oRoot;
+ for iRow in range(1, len(aaoRows)):
+ aoRow = aaoRows[iRow];
+ oCur = TestResultDataEx().initFromDbRow(aoRow);
+ self._fetchResultTreeNodeExtras(oCur, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
+
+ # Figure out and vet the parent.
+ if oParent.idTestResult != oCur.idTestResultParent:
+ oParent = dLookup.get(oCur.idTestResultParent, None);
+ if oParent is None:
+ raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.'
+ % (oCur.idTestResult, oCur.idTestResultParent,));
+ if oParent.iNestingDepth + 1 != oCur.iNestingDepth:
+ raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)'
+ % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,));
+
+ # Link it up.
+ oCur.oParent = oParent;
+ oParent.aoChildren.append(oCur);
+ dLookup[oCur.idTestResult] = oCur;
+
+ return (oRoot, dLookup);
+
+ def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles, fHasReasons):
+ """
+ fetchResultTree worker that fetches values, message and files for the
+ specified node.
+ """
+ assert(oCurNode.aoValues == []);
+ assert(oCurNode.aoMsgs == []);
+ assert(oCurNode.aoFiles == []);
+ assert(oCurNode.oReason is None);
+
+ if fHasValues:
+ self._oDb.execute('SELECT TestResultValues.*,\n'
+ ' TestResultStrTab.sValue\n'
+ 'FROM TestResultValues, TestResultStrTab\n'
+ 'WHERE TestResultValues.idTestResult = %s\n'
+ ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n'
+ 'ORDER BY idTestResultValue ASC\n'
+ , ( oCurNode.idTestResult, ));
+ for aoRow in self._oDb.fetchAll():
+ oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow));
+
+ if fHasMsgs:
+ self._oDb.execute('SELECT TestResultMsgs.*,\n'
+ ' TestResultStrTab.sValue\n'
+ 'FROM TestResultMsgs, TestResultStrTab\n'
+ 'WHERE TestResultMsgs.idTestResult = %s\n'
+ ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n'
+ 'ORDER BY idTestResultMsg ASC\n'
+ , ( oCurNode.idTestResult, ));
+ for aoRow in self._oDb.fetchAll():
+ oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow));
+
+ if fHasFiles:
+ self._oDb.execute('SELECT TestResultFiles.*,\n'
+ ' StrTabFile.sValue AS sFile,\n'
+ ' StrTabDesc.sValue AS sDescription,\n'
+ ' StrTabKind.sValue AS sKind,\n'
+ ' StrTabMime.sValue AS sMime\n'
+ 'FROM TestResultFiles,\n'
+ ' TestResultStrTab AS StrTabFile,\n'
+ ' TestResultStrTab AS StrTabDesc,\n'
+ ' TestResultStrTab AS StrTabKind,\n'
+ ' TestResultStrTab AS StrTabMime\n'
+ 'WHERE TestResultFiles.idTestResult = %s\n'
+ ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
+ ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
+ ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
+ ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
+ 'ORDER BY idTestResultFile ASC\n'
+ , ( oCurNode.idTestResult, ));
+ for aoRow in self._oDb.fetchAll():
+ oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));
+
+ if fHasReasons:
+ if self.oFailureReasonLogic is None:
+ self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
+ if self.oUserAccountLogic is None:
+ self.oUserAccountLogic = UserAccountLogic(self._oDb);
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestResultFailures\n'
+ 'WHERE idTestResult = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , ( oCurNode.idTestResult, ));
+ if self._oDb.getRowCount() > 0:
+ oCurNode.oReason = TestResultFailureDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oFailureReasonLogic,
+ self.oUserAccountLogic);
+
+ return True;
+
+
+
+ #
+ # TestBoxController interface(s).
+ #
+
+ def _inhumeTestResults(self, aoStack, idTestSet, sError):
+ """
+ The test produces too much output, kill and bury it.
+
+ Note! We leave the test set open, only the test result records are
+ completed. Thus, _getResultStack will return an empty stack and
+ cause XML processing to fail immediately, while we can still
+ record when it actually completed in the test set the normal way.
+ """
+ self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),));
+
+ #
+ # First add a message.
+ #
+ self._newFailureDetails(aoStack[0].idTestResult, idTestSet, sError, None);
+
+ #
+ # The complete all open test results.
+ #
+ for oTestResult in aoStack:
+ oTestResult.cErrors += 1;
+ self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors);
+
+ # A bit of paranoia.
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET cErrors = cErrors + 1,\n'
+ ' enmStatus = \'failure\'::TestStatus_T,\n'
+ ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND enmStatus = \'running\'::TestStatus_T\n'
+ , ( idTestSet, ));
+ self._oDb.commit();
+
+ return None;
+
+ def strTabString(self, sString, fCommit = False):
+ """
+ Gets the string table id for the given string, adding it if new.
+
+ Note! A copy of this code is also in TestSetLogic.
+ """
+ ## @todo move this and make a stored procedure for it.
+ self._oDb.execute('SELECT idStr\n'
+ 'FROM TestResultStrTab\n'
+ 'WHERE sValue = %s'
+ , (sString,));
+ if self._oDb.getRowCount() == 0:
+ self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
+ 'VALUES (%s)\n'
+ 'RETURNING idStr\n'
+ , (sString,));
+ if fCommit:
+ self._oDb.commit();
+ return self._oDb.fetchOne()[0];
+
+ @staticmethod
+ def _stringifyStack(aoStack):
+ """Returns a string rep of the stack."""
+ sRet = '';
+ for i, _ in enumerate(aoStack):
+ sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);
+ return sRet;
+
+ def _getResultStack(self, idTestSet):
+ """
+ Gets the current stack of result sets.
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND enmStatus = \'running\'::TestStatus_T\n'
+ 'ORDER BY idTestResult DESC'
+ , ( idTestSet, ));
+ aoStack = [];
+ for aoRow in self._oDb.fetchAll():
+ aoStack.append(TestResultData().initFromDbRow(aoRow));
+
+ for i, _ in enumerate(aoStack):
+ assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);
+
+ return aoStack;
+
+ def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False):
+ """
+ Creates a new test result.
+ Returns the TestResultData object for the new record.
+ May raise exception on database error.
+ """
+ assert idTestResultParent is not None;
+ assert idTestResultParent > 1;
+
+ #
+ # This isn't necessarily very efficient, but it's necessary to prevent
+ # a wild test or testbox from filling up the database.
+ #
+ sCountName = 'cTestResults';
+ if sCountName not in dCounts:
+ self._oDb.execute('SELECT COUNT(idTestResult)\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestSet = %s\n'
+ , ( idTestSet,));
+ dCounts[sCountName] = self._oDb.fetchOne()[0];
+ dCounts[sCountName] += 1;
+ if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS:
+ raise TestResultHangingOffence('Too many sub-tests in total!');
+
+ sCountName = 'cTestResultsIn%d' % (idTestResultParent,);
+ if sCountName not in dCounts:
+ self._oDb.execute('SELECT COUNT(idTestResult)\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestResultParent = %s\n'
+ , ( idTestResultParent,));
+ dCounts[sCountName] = self._oDb.fetchOne()[0];
+ dCounts[sCountName] += 1;
+ if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR:
+ raise TestResultHangingOffence('Too many immediate sub-tests!');
+
+ # This is also a hanging offence.
+ if iNestingDepth > config.g_kcMaxTestResultDepth:
+ raise TestResultHangingOffence('To deep sub-test nesting!');
+
+ # Ditto.
+ if len(sName) > config.g_kcchMaxTestResultName:
+ raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName));
+
+ #
+ # Within bounds, do the job.
+ #
+ idStrName = self.strTabString(sName, fCommit);
+ self._oDb.execute('INSERT INTO TestResults (\n'
+ ' idTestResultParent,\n'
+ ' idTestSet,\n'
+ ' tsCreated,\n'
+ ' idStrName,\n'
+ ' iNestingDepth )\n'
+ 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
+ 'RETURNING *\n'
+ , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) )
+ oData = TestResultData().initFromDbRow(self._oDb.fetchOne());
+
+ self._oDb.maybeCommit(fCommit);
+ return oData;
+
+ def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False):
+ """
+ Creates a test value.
+ May raise exception on database error.
+ """
+
+ #
+ # Bounds checking.
+ #
+ sCountName = 'cTestValues';
+ if sCountName not in dCounts:
+ self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
+ 'FROM TestResultValues, TestResults\n'
+ 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n'
+ ' AND TestResults.idTestSet = %s\n'
+ , ( idTestSet,));
+ dCounts[sCountName] = self._oDb.fetchOne()[0];
+ dCounts[sCountName] += 1;
+ if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS:
+ raise TestResultHangingOffence('Too many values in total!');
+
+ sCountName = 'cTestValuesIn%d' % (idTestResult,);
+ if sCountName not in dCounts:
+ self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
+ 'FROM TestResultValues\n'
+ 'WHERE idTestResult = %s\n'
+ , ( idTestResult,));
+ dCounts[sCountName] = self._oDb.fetchOne()[0];
+ dCounts[sCountName] += 1;
+ if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR:
+ raise TestResultHangingOffence('Too many immediate values for one test result!');
+
+ if len(sName) > config.g_kcchMaxTestValueName:
+ raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName));
+
+ #
+ # Do the job.
+ #
+ iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE);
+
+ idStrName = self.strTabString(sName, fCommit);
+ if tsCreated is None:
+ self._oDb.execute('INSERT INTO TestResultValues (\n'
+ ' idTestResult,\n'
+ ' idTestSet,\n'
+ ' idStrName,\n'
+ ' lValue,\n'
+ ' iUnit)\n'
+ 'VALUES ( %s, %s, %s, %s, %s )\n'
+ , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) );
+ else:
+ self._oDb.execute('INSERT INTO TestResultValues (\n'
+ ' idTestResult,\n'
+ ' idTestSet,\n'
+ ' tsCreated,\n'
+ ' idStrName,\n'
+ ' lValue,\n'
+ ' iUnit)\n'
+ 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n'
+ , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) );
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def _newFailureDetails(self, idTestResult, idTestSet, sText, dCounts, tsCreated = None, fCommit = False):
+ """
+ Creates a record detailing cause of failure.
+ May raise exception on database error.
+ """
+
+ #
+ # Overflow protection.
+ #
+ if dCounts is not None:
+ sCountName = 'cTestMsgsIn%d' % (idTestResult,);
+ if sCountName not in dCounts:
+ self._oDb.execute('SELECT COUNT(idTestResultMsg)\n'
+ 'FROM TestResultMsgs\n'
+ 'WHERE idTestResult = %s\n'
+ , ( idTestResult,));
+ dCounts[sCountName] = self._oDb.fetchOne()[0];
+ dCounts[sCountName] += 1;
+ if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR:
+ raise TestResultHangingOffence('Too many messages under for one test result!');
+
+ if len(sText) > config.g_kcchMaxTestMsg:
+ raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText));
+
+ #
+ # Do the job.
+ #
+ idStrMsg = self.strTabString(sText, fCommit);
+ if tsCreated is None:
+ self._oDb.execute('INSERT INTO TestResultMsgs (\n'
+ ' idTestResult,\n'
+ ' idTestSet,\n'
+ ' idStrMsg,\n'
+ ' enmLevel)\n'
+ 'VALUES ( %s, %s, %s, %s)\n'
+ , ( idTestResult, idTestSet, idStrMsg, 'failure',) );
+ else:
+ self._oDb.execute('INSERT INTO TestResultMsgs (\n'
+ ' idTestResult,\n'
+ ' idTestSet,\n'
+ ' tsCreated,\n'
+ ' idStrMsg,\n'
+ ' enmLevel)\n'
+ 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
+ , ( idTestResult, idTestSet, tsCreated, idStrMsg, 'failure',) );
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+
+ def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False):
+ """
+ Completes a test result. Updates the oTestResult object.
+ May raise exception on database error.
+ """
+ self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s'
+ % (cErrors, tsDone, enmStatus, oTestResult,));
+
+ #
+ # Sanity check: No open sub tests (aoStack should make sure about this!).
+ #
+ self._oDb.execute('SELECT COUNT(idTestResult)\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestResultParent = %s\n'
+ ' AND enmStatus = %s\n'
+ , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,));
+ cOpenSubTest = self._oDb.fetchOne()[0];
+ assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,);
+ assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running;
+
+ #
+ # Make sure the reporter isn't lying about successes or error counts.
+ #
+ self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestResultParent = %s\n'
+ , ( oTestResult.idTestResult, ));
+ cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors;
+ cErrors = max(cErrors, cMinErrors);
+ if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success:
+ enmStatus = TestResultData.ksTestStatus_Failure
+
+ #
+ # Do the update.
+ #
+ if tsDone is None:
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET cErrors = %s,\n'
+ ' enmStatus = %s,\n'
+ ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
+ 'WHERE idTestResult = %s\n'
+ 'RETURNING tsElapsed'
+ , ( cErrors, enmStatus, oTestResult.idTestResult,) );
+ else:
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET cErrors = %s,\n'
+ ' enmStatus = %s,\n'
+ ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n'
+ 'WHERE idTestResult = %s\n'
+ 'RETURNING tsElapsed'
+ , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) );
+
+ oTestResult.tsElapsed = self._oDb.fetchOne()[0];
+ oTestResult.enmStatus = enmStatus;
+ oTestResult.cErrors = cErrors;
+
+ self._oDb.maybeCommit(fCommit);
+ return None;
+
+ def _doPopHint(self, aoStack, cStackEntries, dCounts, idTestSet):
+ """ Executes a PopHint. """
+ assert cStackEntries >= 0;
+ while len(aoStack) > cStackEntries:
+ if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running:
+ self._newFailureDetails(aoStack[0].idTestResult, idTestSet, 'XML error: Missing </Test>', dCounts);
+ self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1,
+ enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
+ aoStack.pop(0);
+ return True;
+
+
+ @staticmethod
+ def _validateElement(sName, dAttribs, fClosed):
+ """
+ Validates an element and its attributes.
+ """
+
+ #
+ # Validate attributes by name.
+ #
+
+ # Validate integer attributes.
+ for sAttr in [ 'errors', 'testdepth' ]:
+ if sAttr in dAttribs:
+ try:
+ _ = int(dAttribs[sAttr]);
+ except:
+ return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
+
+ # Validate long attributes.
+ for sAttr in [ 'value', ]:
+ if sAttr in dAttribs:
+ try:
+ _ = long(dAttribs[sAttr]); # pylint: disable=redefined-variable-type
+ except:
+ return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
+
+ # Validate string attributes.
+ for sAttr in [ 'name', 'text' ]: # 'unit' can be zero length.
+ if sAttr in dAttribs and not dAttribs[sAttr]:
+ return 'Element %s has an empty %s attribute value.' % (sName, sAttr,);
+
+ # Validate the timestamp attribute.
+ if 'timestamp' in dAttribs:
+ (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False);
+ if sError is not None:
+ return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,);
+
+
+ #
+ # Check that attributes that are required are present.
+ # We ignore extra attributes.
+ #
+ dElementAttribs = \
+ {
+ 'Test': [ 'timestamp', 'name', ],
+ 'Value': [ 'timestamp', 'name', 'unit', 'value', ],
+ 'FailureDetails': [ 'timestamp', 'text', ],
+ 'Passed': [ 'timestamp', ],
+ 'Skipped': [ 'timestamp', ],
+ 'Failed': [ 'timestamp', 'errors', ],
+ 'TimedOut': [ 'timestamp', 'errors', ],
+ 'End': [ 'timestamp', ],
+ 'PushHint': [ 'testdepth', ],
+ 'PopHint': [ 'testdepth', ],
+ };
+ if sName not in dElementAttribs:
+ return 'Unknown element "%s".' % (sName,);
+ for sAttr in dElementAttribs[sName]:
+ if sAttr not in dAttribs:
+ return 'Element %s requires attribute "%s".' % (sName, sAttr);
+
+ #
+ # Only the Test element can (and must) remain open.
+ #
+ if sName == 'Test' and fClosed:
+ return '<Test/> is not allowed.';
+ if sName != 'Test' and not fClosed:
+ return 'All elements except <Test> must be closed.';
+
+ return None;
+
+ @staticmethod
+ def _parseElement(sElement):
+ """
+ Parses an element.
+
+ """
+ #
+ # Element level bits.
+ #
+ sName = sElement.split()[0];
+ sElement = sElement[len(sName):];
+
+ fClosed = sElement[-1] == '/';
+ if fClosed:
+ sElement = sElement[:-1];
+
+ #
+ # Attributes.
+ #
+ sError = None;
+ dAttribs = {};
+ sElement = sElement.strip();
+ while sElement:
+ # Extract attribute name.
+ off = sElement.find('=');
+ if off < 0 or not sElement[:off].isalnum():
+ sError = 'Attributes shall have alpha numberical names and have values.';
+ break;
+ sAttr = sElement[:off];
+
+ # Extract attribute value.
+ if off + 2 >= len(sElement) or sElement[off + 1] != '"':
+ sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,);
+ break;
+ off += 2;
+ offEndQuote = sElement.find('"', off);
+ if offEndQuote < 0:
+ sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,);
+ break;
+ sValue = sElement[off:offEndQuote];
+
+ # Check for duplicates.
+ if sAttr in dAttribs:
+ sError = 'Attribute "%s" appears more than once.' % (sAttr,);
+ break;
+
+ # Unescape the value.
+ sValue = sValue.replace('&lt;', '<');
+ sValue = sValue.replace('&gt;', '>');
+ sValue = sValue.replace('&apos;', '\'');
+ sValue = sValue.replace('&quot;', '"');
+ sValue = sValue.replace('&#xA;', '\n');
+ sValue = sValue.replace('&#xD;', '\r');
+ sValue = sValue.replace('&amp;', '&'); # last
+
+ # Done.
+ dAttribs[sAttr] = sValue;
+
+ # advance
+ sElement = sElement[offEndQuote + 1:];
+ sElement = sElement.lstrip();
+
+ #
+ # Validate the element before we return.
+ #
+ if sError is None:
+ sError = TestResultLogic._validateElement(sName, dAttribs, fClosed);
+
+ return (sName, dAttribs, sError)
+
+ def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts):
+ """
+ Worker for processXmlStream that handles one element.
+
+ Returns None on success, error string on bad XML or similar.
+ Raises exception on hanging offence and on database error.
+ """
+ if sName == 'Test':
+ iNestingDepth = aoStack[0].iNestingDepth + 1 if aoStack else 0;
+ aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet,
+ tsCreated = dAttribs['timestamp'], sName = dAttribs['name'],
+ iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) );
+
+ elif sName == 'Value':
+ self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'],
+ sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']),
+ dCounts = dCounts, fCommit = True);
+
+ elif sName == 'FailureDetails':
+ self._newFailureDetails(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet,
+ tsCreated = dAttribs['timestamp'], sText = dAttribs['text'], dCounts = dCounts,
+ fCommit = True);
+
+ elif sName == 'Passed':
+ self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
+ enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
+
+ elif sName == 'Skipped':
+ self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
+ enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True);
+
+ elif sName == 'Failed':
+ self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
+ enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
+
+ elif sName == 'TimedOut':
+ self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
+ enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True);
+
+ elif sName == 'End':
+ self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
+ cErrors = int(dAttribs.get('errors', '1')),
+ enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
+
+ elif sName == 'PushHint':
+ if len(aaiHints) > 1:
+ return 'PushHint cannot be nested.'
+
+ aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]);
+
+ elif sName == 'PopHint':
+ if not aaiHints:
+ return 'No hint to pop.'
+
+ iDesiredTestDepth = int(dAttribs['testdepth']);
+ cStackEntries, iTestDepth = aaiHints.pop(0);
+ self._doPopHint(aoStack, cStackEntries, dCounts, idTestSet); # Fake the necessary '<End/></Test>' tags.
+ if iDesiredTestDepth != iTestDepth:
+ return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth);
+ else:
+ return 'Unexpected element "%s".' % (sName,);
+ return None;
+
+
+ def processXmlStream(self, sXml, idTestSet):
+ """
+ Processes the "XML" stream section given in sXml.
+
+ The sXml isn't a complete XML document, even should we save up all sXml
+ for a given set, they may not form a complete and well formed XML
+ document since the test may be aborted, abend or simply be buggy. We
+ therefore do our own parsing and treat the XML tags as commands more
+ than anything else.
+
+ Returns (sError, fUnforgivable), where sError is None on success.
+ May raise database exception.
+ """
+ aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom.
+ if not aoStack:
+ return ('No open results', True);
+ self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),));
+ #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),));
+ #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,));
+
+ dCounts = {};
+ aaiHints = [];
+ sError = None;
+
+ fExpectCloseTest = False;
+ sXml = sXml.strip();
+ while sXml:
+ if sXml.startswith('</Test>'): # Only closing tag.
+ offNext = len('</Test>');
+ if len(aoStack) <= 1:
+ sError = 'Trying to close the top test results.'
+ break;
+ # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>,
+ # <TimedOut/> or <Skipped/> tag earlier in this call!
+ if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest:
+ sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.';
+ break;
+ aoStack.pop(0);
+ fExpectCloseTest = False;
+
+ elif fExpectCloseTest:
+ sError = 'Expected </Test>.'
+ break;
+
+ elif sXml.startswith('<?xml '): # Ignore (included files).
+ offNext = sXml.find('?>');
+ if offNext < 0:
+ sError = 'Unterminated <?xml ?> element.';
+ break;
+ offNext += 2;
+
+ elif sXml[0] == '<':
+ # Parse and check the tag.
+ if not sXml[1].isalpha():
+ sError = 'Malformed element.';
+ break;
+ offNext = sXml.find('>')
+ if offNext < 0:
+ sError = 'Unterminated element.';
+ break;
+ (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]);
+ offNext += 1;
+ if sError is not None:
+ break;
+
+ # Handle it.
+ try:
+ sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts);
+ except TestResultHangingOffence as oXcpt:
+ self._inhumeTestResults(aoStack, idTestSet, str(oXcpt));
+ return (str(oXcpt), True);
+
+
+ fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ];
+ else:
+ sError = 'Unexpected content.';
+ break;
+
+ # Advance.
+ sXml = sXml[offNext:];
+ sXml = sXml.lstrip();
+
+ #
+ # Post processing checks.
+ #
+ if sError is None and fExpectCloseTest:
+ sError = 'Expected </Test> before the end of the XML section.'
+ elif sError is None and aaiHints:
+ sError = 'Expected </PopHint> before the end of the XML section.'
+ if aaiHints:
+ self._doPopHint(aoStack, aaiHints[-1][0], dCounts, idTestSet);
+
+ #
+ # Log the error.
+ #
+ if sError is not None:
+ SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed,
+ 'idTestSet=%s idTestResult=%s XML="%s" %s'
+ % ( idTestSet,
+ aoStack[0].idTestResult if aoStack else -1,
+ sXml[:min(len(sXml), 30)],
+ sError, ),
+ cHoursRepeat = 6, fCommit = True);
+ return (sError, False);
+
+
+
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class TestResultDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestResultData(),];
+
+class TestResultValueDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestResultValueData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/testset.py b/src/VBox/ValidationKit/testmanager/core/testset.py
new file mode 100755
index 00000000..e9e408fe
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/testset.py
@@ -0,0 +1,869 @@
+# -*- coding: utf-8 -*-
+# $Id: testset.py $
+
+"""
+Test Manager - TestSet.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os;
+import zipfile;
+import unittest;
+
+# Validation Kit imports.
+from common import utils;
+from testmanager import config;
+from testmanager.core import db;
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, \
+ TMExceptionBase, TMTooManyRows, TMRowNotFound;
+from testmanager.core.testbox import TestBoxData;
+from testmanager.core.testresults import TestResultFileDataEx;
+
+
+class TestSetData(ModelDataBase):
+ """
+ TestSet Data.
+ """
+
+ ## @name TestStatus_T
+ # @{
+ ksTestStatus_Running = 'running';
+ ksTestStatus_Success = 'success';
+ ksTestStatus_Skipped = 'skipped';
+ ksTestStatus_BadTestBox = 'bad-testbox';
+ ksTestStatus_Aborted = 'aborted';
+ ksTestStatus_Failure = 'failure';
+ ksTestStatus_TimedOut = 'timed-out';
+ ksTestStatus_Rebooted = 'rebooted';
+ ## @}
+
+ ## List of relatively harmless (to testgroup/case) statuses.
+ kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ];
+ ## List of bad statuses.
+ kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ];
+
+ ksIdAttr = 'idTestSet';
+
+ ksParam_idTestSet = 'TestSet_idTestSet';
+ ksParam_tsConfig = 'TestSet_tsConfig';
+ ksParam_tsCreated = 'TestSet_tsCreated';
+ ksParam_tsDone = 'TestSet_tsDone';
+ ksParam_enmStatus = 'TestSet_enmStatus';
+ ksParam_idBuild = 'TestSet_idBuild';
+ ksParam_idBuildCategory = 'TestSet_idBuildCategory';
+ ksParam_idBuildTestSuite = 'TestSet_idBuildTestSuite';
+ ksParam_idGenTestBox = 'TestSet_idGenTestBox';
+ ksParam_idTestBox = 'TestSet_idTestBox';
+ ksParam_idSchedGroup = 'TestSet_idSchedGroup';
+ ksParam_idTestGroup = 'TestSet_idTestGroup';
+ ksParam_idGenTestCase = 'TestSet_idGenTestCase';
+ ksParam_idTestCase = 'TestSet_idTestCase';
+ ksParam_idGenTestCaseArgs = 'TestSet_idGenTestCaseArgs';
+ ksParam_idTestCaseArgs = 'TestSet_idTestCaseArgs';
+ ksParam_idTestResult = 'TestSet_idTestResult';
+ ksParam_sBaseFilename = 'TestSet_sBaseFilename';
+ ksParam_iGangMemberNo = 'TestSet_iGangMemberNo';
+ ksParam_idTestSetGangLeader = 'TestSet_idTestSetGangLeader';
+
+ kasAllowNullAttributes = [ 'tsDone', 'idBuildTestSuite', 'idTestSetGangLeader' ];
+ kasValidValues_enmStatus = [
+ ksTestStatus_Running,
+ ksTestStatus_Success,
+ ksTestStatus_Skipped,
+ ksTestStatus_BadTestBox,
+ ksTestStatus_Aborted,
+ ksTestStatus_Failure,
+ ksTestStatus_TimedOut,
+ ksTestStatus_Rebooted,
+ ];
+ kiMin_iGangMemberNo = 0;
+ kiMax_iGangMemberNo = 1023;
+
+
+ kcDbColumns = 20;
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.idTestSet = None;
+ self.tsConfig = None;
+ self.tsCreated = None;
+ self.tsDone = None;
+ self.enmStatus = 'running';
+ self.idBuild = None;
+ self.idBuildCategory = None;
+ self.idBuildTestSuite = None;
+ self.idGenTestBox = None;
+ self.idTestBox = None;
+ self.idSchedGroup = None;
+ self.idTestGroup = None;
+ self.idGenTestCase = None;
+ self.idTestCase = None;
+ self.idGenTestCaseArgs = None;
+ self.idTestCaseArgs = None;
+ self.idTestResult = None;
+ self.sBaseFilename = None;
+ self.iGangMemberNo = 0;
+ self.idTestSetGangLeader = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Internal worker for initFromDbWithId and initFromDbWithGenId as well as
+ TestBoxSetLogic.
+ """
+
+ if aoRow is None:
+ raise TMRowNotFound('TestSet not found.');
+
+ self.idTestSet = aoRow[0];
+ self.tsConfig = aoRow[1];
+ self.tsCreated = aoRow[2];
+ self.tsDone = aoRow[3];
+ self.enmStatus = aoRow[4];
+ self.idBuild = aoRow[5];
+ self.idBuildCategory = aoRow[6];
+ self.idBuildTestSuite = aoRow[7];
+ self.idGenTestBox = aoRow[8];
+ self.idTestBox = aoRow[9];
+ self.idSchedGroup = aoRow[10];
+ self.idTestGroup = aoRow[11];
+ self.idGenTestCase = aoRow[12];
+ self.idTestCase = aoRow[13];
+ self.idGenTestCaseArgs = aoRow[14];
+ self.idTestCaseArgs = aoRow[15];
+ self.idTestResult = aoRow[16];
+ self.sBaseFilename = aoRow[17];
+ self.iGangMemberNo = aoRow[18];
+ self.idTestSetGangLeader = aoRow[19];
+ return self;
+
+
+ def initFromDbWithId(self, oDb, idTestSet):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute('SELECT *\n'
+ 'FROM TestSets\n'
+ 'WHERE idTestSet = %s\n'
+ , (idTestSet, ) );
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('idTestSet=%s not found' % (idTestSet,));
+ return self.initFromDbRow(aoRow);
+
+
+ def openFile(self, sFilename, sMode = 'rb'):
+ """
+ Opens a file.
+
+ Returns (oFile, cbFile, fIsStream) on success.
+ Returns (None, sErrorMsg, None) on failure.
+ Will not raise exceptions, unless the class instance is invalid.
+ """
+ assert sMode in [ 'rb', 'r', 'rU' ];
+
+ # Try raw file first.
+ sFile1 = os.path.join(config.g_ksFileAreaRootDir, self.sBaseFilename + '-' + sFilename);
+ try:
+ oFile = open(sFile1, sMode); # pylint: disable=consider-using-with,unspecified-encoding
+ return (oFile, os.fstat(oFile.fileno()).st_size, False);
+ except Exception as oXcpt1:
+ # Try the zip archive next.
+ sFile2 = os.path.join(config.g_ksZipFileAreaRootDir, self.sBaseFilename + '.zip');
+ try:
+ oZipFile = zipfile.ZipFile(sFile2, 'r'); # pylint: disable=consider-using-with
+ oFile = oZipFile.open(sFilename, sMode if sMode != 'rb' else 'r'); # pylint: disable=consider-using-with
+ cbFile = oZipFile.getinfo(sFilename).file_size;
+ return (oFile, cbFile, True);
+ except Exception as oXcpt2:
+ # Construct a meaningful error message.
+ try:
+ if os.path.exists(sFile1):
+ return (None, 'Error opening "%s": %s' % (sFile1, oXcpt1), None);
+ if not os.path.exists(sFile2):
+ return (None, 'File "%s" not found. [%s, %s]' % (sFilename, sFile1, sFile2,), None);
+ return (None, 'Error opening "%s" inside "%s": %s' % (sFilename, sFile2, oXcpt2), None);
+ except Exception as oXcpt3:
+ return (None, 'OMG! %s; %s; %s' % (oXcpt1, oXcpt2, oXcpt3,), None);
+ return (None, 'Code not reachable!', None);
+
+ def createFile(self, sFilename, sMode = 'wb'):
+ """
+ Creates a new file.
+
+ Returns oFile on success.
+ Returns sErrorMsg on failure.
+ """
+ assert sMode in [ 'wb', 'w', 'wU' ];
+
+ # Try raw file first.
+ sFile1 = os.path.join(config.g_ksFileAreaRootDir, self.sBaseFilename + '-' + sFilename);
+ try:
+ if not os.path.exists(os.path.dirname(sFile1)):
+ os.makedirs(os.path.dirname(sFile1), 0o755);
+ oFile = open(sFile1, sMode); # pylint: disable=consider-using-with,unspecified-encoding
+ except Exception as oXcpt1:
+ return str(oXcpt1);
+ return oFile;
+
+ @staticmethod
+ def findLogOffsetForTimestamp(sLogContent, tsTimestamp, offStart = 0, fAfter = False):
+ """
+ Log parsing utility function for finding the offset for the given timestamp.
+
+ We ASSUME the log lines are prefixed with UTC timestamps on the format
+ '09:43:55.789353'.
+
+ Return index into the sLogContent string, 0 if not found.
+ """
+ # Turn tsTimestamp into a string compatible with what we expect to find in the log.
+ oTsZulu = db.dbTimestampToZuluDatetime(tsTimestamp);
+ sWantedTs = oTsZulu.strftime('%H:%M:%S.%f');
+ assert len(sWantedTs) == 15;
+
+ # Now loop thru the string, line by line.
+ offRet = offStart;
+ off = offStart;
+ while True:
+ sThisTs = sLogContent[off : off + 15];
+ if len(sThisTs) >= 15 \
+ and sThisTs[2] == ':' \
+ and sThisTs[5] == ':' \
+ and sThisTs[8] == '.' \
+ and sThisTs[14] in '0123456789':
+ if sThisTs < sWantedTs:
+ offRet = off;
+ elif sThisTs == sWantedTs:
+ if not fAfter:
+ return off;
+ offRet = off;
+ else:
+ if fAfter:
+ offRet = off;
+ break;
+
+ # next line.
+ off = sLogContent.find('\n', off);
+ if off < 0:
+ if fAfter:
+ offRet = len(sLogContent);
+ break;
+ off += 1;
+
+ return offRet;
+
+ @staticmethod
+ def extractLogSection(sLogContent, tsStart, tsLast):
+ """
+ Returns log section from tsStart to tsLast (or all if we cannot make sense of it).
+ """
+ offStart = TestSetData.findLogOffsetForTimestamp(sLogContent, tsStart);
+ offEnd = TestSetData.findLogOffsetForTimestamp(sLogContent, tsLast, offStart, fAfter = True);
+ return sLogContent[offStart : offEnd];
+
+ @staticmethod
+ def extractLogSectionElapsed(sLogContent, tsStart, tsElapsed):
+ """
+ Returns log section from tsStart and tsElapsed forward (or all if we cannot make sense of it).
+ """
+ tsStart = db.dbTimestampToZuluDatetime(tsStart);
+ tsLast = tsStart + tsElapsed;
+ return TestSetData.extractLogSection(sLogContent, tsStart, tsLast);
+
+
+
+class TestSetLogic(ModelLogicBase):
+ """
+ TestSet logic.
+ """
+
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb);
+
+
+ def tryFetch(self, idTestSet):
+ """
+ Attempts to fetch a test set.
+
+ Returns a TestSetData object on success.
+ Returns None if no status was found.
+ Raises exception on other errors.
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestSets\n'
+ 'WHERE idTestSet = %s\n',
+ (idTestSet,));
+ if self._oDb.getRowCount() == 0:
+ return None;
+ oData = TestSetData();
+ return oData.initFromDbRow(self._oDb.fetchOne());
+
+ def strTabString(self, sString, fCommit = False):
+ """
+ Gets the string table id for the given string, adding it if new.
+ """
+ ## @todo move this and make a stored procedure for it.
+ self._oDb.execute('SELECT idStr\n'
+ 'FROM TestResultStrTab\n'
+ 'WHERE sValue = %s'
+ , (sString,));
+ if self._oDb.getRowCount() == 0:
+ self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
+ 'VALUES (%s)\n'
+ 'RETURNING idStr\n'
+ , (sString,));
+ if fCommit:
+ self._oDb.commit();
+ return self._oDb.fetchOne()[0];
+
+ def complete(self, idTestSet, sStatus, fCommit = False):
+ """
+ Completes the testset.
+ Returns the test set ID of the gang leader, None if no gang involvement.
+ Raises exceptions on database errors and invalid input.
+ """
+
+ assert sStatus != TestSetData.ksTestStatus_Running;
+
+ #
+ # Get the basic test set data and check if there is anything to do here.
+ #
+ oData = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ if oData.enmStatus != TestSetData.ksTestStatus_Running:
+ raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus));
+ if oData.idTestResult is None:
+ raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,));
+
+ #
+ # Close open sub test results, count these as errors.
+ # Note! No need to propagate error counts here. Only one tree line will
+ # have open sets, and it will go all the way to the root.
+ #
+ self._oDb.execute('SELECT idTestResult\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND enmStatus = %s\n'
+ ' AND idTestResult <> %s\n'
+ 'ORDER BY idTestResult DESC\n'
+ , (idTestSet, TestSetData.ksTestStatus_Running, oData.idTestResult));
+ aaoRows = self._oDb.fetchAll();
+ if aaoRows:
+ idStr = self.strTabString('Unclosed test result', fCommit = fCommit);
+ for aoRow in aaoRows:
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET enmStatus = \'failure\',\n'
+ ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
+ ' cErrors = cErrors + 1\n'
+ 'WHERE idTestResult = %s\n'
+ , (aoRow[0],));
+ self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n'
+ 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
+ , (aoRow[0], idTestSet, idStr,));
+
+ #
+ # If it's a success result, check it against error counters.
+ #
+ if sStatus not in TestSetData.kasBadTestStatuses:
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND cErrors > 0\n'
+ , (idTestSet,));
+ cErrors = self._oDb.fetchOne()[0];
+ if cErrors > 0:
+ sStatus = TestSetData.ksTestStatus_Failure;
+
+ #
+ # If it's an pure 'failure', check for timeouts and propagate it.
+ #
+ if sStatus == TestSetData.ksTestStatus_Failure:
+ self._oDb.execute('SELECT COUNT(*)\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND enmStatus = %s\n'
+ , ( idTestSet, TestSetData.ksTestStatus_TimedOut, ));
+ if self._oDb.fetchOne()[0] > 0:
+ sStatus = TestSetData.ksTestStatus_TimedOut;
+
+ #
+ # Complete the top level test result and then the test set.
+ #
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET cErrors = (SELECT COALESCE(SUM(cErrors), 0)\n'
+ ' FROM TestResults\n'
+ ' WHERE idTestResultParent = %s)\n'
+ 'WHERE idTestResult = %s\n'
+ 'RETURNING cErrors\n'
+ , (oData.idTestResult, oData.idTestResult));
+ cErrors = self._oDb.fetchOne()[0];
+ if cErrors == 0 and sStatus in TestSetData.kasBadTestStatuses:
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET cErrors = 1\n'
+ 'WHERE idTestResult = %s\n'
+ , (oData.idTestResult,));
+ elif cErrors > 0 and sStatus not in TestSetData.kasBadTestStatuses:
+ sStatus = TestSetData.ksTestStatus_Failure; # Impossible.
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET enmStatus = %s,\n'
+ ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
+ 'WHERE idTestResult = %s\n'
+ , (sStatus, oData.idTestResult,));
+
+ self._oDb.execute('UPDATE TestSets\n'
+ 'SET enmStatus = %s,\n'
+ ' tsDone = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestSet = %s\n'
+ , (sStatus, idTestSet,));
+
+ self._oDb.maybeCommit(fCommit);
+ return oData.idTestSetGangLeader;
+
+ def completeAsAbandoned(self, idTestSet, fCommit = False):
+ """
+ Completes the testset as abandoned if necessary.
+
+ See scenario #9:
+ file://../../docs/AutomaticTestingRevamp.html#cleaning-up-abandond-testcase
+
+ Returns True if successfully completed as abandond, False if it's already
+ completed, and raises exceptions under exceptional circumstances.
+ """
+
+ #
+ # Get the basic test set data and check if there is anything to do here.
+ #
+ oData = self.tryFetch(idTestSet);
+ if oData is None:
+ return False;
+ if oData.enmStatus != TestSetData.ksTestStatus_Running:
+ return False;
+
+ if oData.idTestResult is not None:
+ #
+ # Clean up test results, adding a message why they failed.
+ #
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET enmStatus = \'failure\',\n'
+ ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
+ ' cErrors = cErrors + 1\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND enmStatus = \'running\'::TestStatus_T\n'
+ , (idTestSet,));
+
+ idStr = self.strTabString('The test was abandond by the testbox', fCommit = fCommit);
+ self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n'
+ 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
+ , (oData.idTestResult, idTestSet, idStr,));
+
+ #
+ # Complete the testset.
+ #
+ self._oDb.execute('UPDATE TestSets\n'
+ 'SET enmStatus = \'failure\',\n'
+ ' tsDone = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND enmStatus = \'running\'::TestStatus_T\n'
+ , (idTestSet,));
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def completeAsGangGatheringTimeout(self, idTestSet, fCommit = False):
+ """
+ Completes the testset with a gang-gathering timeout.
+ Raises exceptions on database errors and invalid input.
+ """
+ #
+ # Get the basic test set data and check if there is anything to do here.
+ #
+ oData = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ if oData.enmStatus != TestSetData.ksTestStatus_Running:
+ raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus));
+ if oData.idTestResult is None:
+ raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,));
+
+ #
+ # Complete the top level test result and then the test set.
+ #
+ self._oDb.execute('UPDATE TestResults\n'
+ 'SET enmStatus = \'failure\',\n'
+ ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n'
+ ' cErrors = cErrors + 1\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND enmStatus = \'running\'::TestStatus_T\n'
+ , (idTestSet,));
+
+ idStr = self.strTabString('Gang gathering timed out', fCommit = fCommit);
+ self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n'
+ 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n'
+ , (oData.idTestResult, idTestSet, idStr,));
+
+ self._oDb.execute('UPDATE TestSets\n'
+ 'SET enmStatus = \'failure\',\n'
+ ' tsDone = CURRENT_TIMESTAMP\n'
+ 'WHERE idTestSet = %s\n'
+ , (idTestSet,));
+
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def createFile(self, oTestSet, sName, sMime, sKind, sDesc, cbFile, fCommit = False): # pylint: disable=too-many-locals
+ """
+ Creates a file and associating with the current test result record in
+ the test set.
+
+ Returns file object that the file content can be written to.
+ Raises exception on database error, I/O errors, if there are too many
+ files in the test set or if they take up too much disk space.
+
+ The caller (testboxdisp.py) is expected to do basic input validation,
+ so we skip that and get on with the bits only we can do.
+ """
+
+ #
+ # Furhter input and limit checks.
+ #
+ if oTestSet.enmStatus != TestSetData.ksTestStatus_Running:
+ raise TMExceptionBase('Cannot create files on a test set with status "%s".' % (oTestSet.enmStatus,));
+
+ self._oDb.execute('SELECT TestResultStrTab.sValue\n'
+ 'FROM TestResultFiles,\n'
+ ' TestResults,\n'
+ ' TestResultStrTab\n'
+ 'WHERE TestResults.idTestSet = %s\n'
+ ' AND TestResultFiles.idTestResult = TestResults.idTestResult\n'
+ ' AND TestResultStrTab.idStr = TestResultFiles.idStrFile\n'
+ , ( oTestSet.idTestSet,));
+ if self._oDb.getRowCount() + 1 > config.g_kcMaxUploads:
+ raise TMExceptionBase('Uploaded too many files already (%d).' % (self._oDb.getRowCount(),));
+
+ dFiles = {}
+ cbTotalFiles = 0;
+ for aoRow in self._oDb.fetchAll():
+ dFiles[aoRow[0].lower()] = 1; # For determining a unique filename further down.
+ sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-' + aoRow[0]);
+ try:
+ cbTotalFiles += os.path.getsize(sFile);
+ except:
+ cbTotalFiles += config.g_kcMbMaxUploadSingle * 1048576;
+ if (cbTotalFiles + cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadTotal:
+ raise TMExceptionBase('Will exceed total upload limit: %u bytes + %u bytes > %s MiB.' \
+ % (cbTotalFiles, cbFile, config.g_kcMbMaxUploadTotal));
+
+ #
+ # Create a new file.
+ #
+ self._oDb.execute('SELECT idTestResult\n'
+ 'FROM TestResults\n'
+ 'WHERE idTestSet = %s\n'
+ ' AND enmStatus = \'running\'::TestStatus_T\n'
+ 'ORDER BY idTestResult DESC\n'
+ 'LIMIT 1\n'
+ % ( oTestSet.idTestSet, ));
+ if self._oDb.getRowCount() < 1:
+ raise TMExceptionBase('No open test results - someone committed a capital offence or we ran into a race.');
+ idTestResult = self._oDb.fetchOne()[0];
+
+ if sName.lower() in dFiles:
+ # Note! There is in theory a race here, but that's something the
+ # test driver doing parallel upload with non-unique names
+ # should worry about. The TD should always avoid this path.
+ sOrgName = sName;
+ for i in range(2, config.g_kcMaxUploads + 6):
+ sName = '%s-%s' % (i, sName,);
+ if sName not in dFiles:
+ break;
+ sName = None;
+ if sName is None:
+ raise TMExceptionBase('Failed to find unique name for %s.' % (sOrgName,));
+
+ self._oDb.execute('INSERT INTO TestResultFiles(idTestResult, idTestSet, idStrFile, idStrDescription,\n'
+ ' idStrKind, idStrMime)\n'
+ 'VALUES (%s, %s, %s, %s, %s, %s)\n'
+ , ( idTestResult,
+ oTestSet.idTestSet,
+ self.strTabString(sName),
+ self.strTabString(sDesc),
+ self.strTabString(sKind),
+ self.strTabString(sMime),
+ ));
+
+ oFile = oTestSet.createFile(sName, 'wb');
+ if utils.isString(oFile):
+ raise TMExceptionBase('Error creating "%s": %s' % (sName, oFile));
+ self._oDb.maybeCommit(fCommit);
+ return oFile;
+
+ def getGang(self, idTestSetGangLeader):
+ """
+ Returns an array of TestBoxData object representing the gang for the given testset.
+ """
+ self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
+ 'FROM TestBoxesWithStrings,\n'
+ ' TestSets'
+ 'WHERE TestSets.idTestSetGangLeader = %s\n'
+ ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox\n'
+ 'ORDER BY iGangMemberNo ASC\n'
+ , ( idTestSetGangLeader,));
+ aaoRows = self._oDb.fetchAll();
+ aoTestBoxes = [];
+ for aoRow in aaoRows:
+ aoTestBoxes.append(TestBoxData().initFromDbRow(aoRow));
+ return aoTestBoxes;
+
+ def getFile(self, idTestSet, idTestResultFile):
+ """
+ Gets the TestResultFileEx corresponding to idTestResultFile.
+
+ Raises an exception if the file wasn't found, doesn't belong to
+ idTestSet, and on DB error.
+ """
+ self._oDb.execute('SELECT TestResultFiles.*,\n'
+ ' StrTabFile.sValue AS sFile,\n'
+ ' StrTabDesc.sValue AS sDescription,\n'
+ ' StrTabKind.sValue AS sKind,\n'
+ ' StrTabMime.sValue AS sMime\n'
+ 'FROM TestResultFiles,\n'
+ ' TestResultStrTab AS StrTabFile,\n'
+ ' TestResultStrTab AS StrTabDesc,\n'
+ ' TestResultStrTab AS StrTabKind,\n'
+ ' TestResultStrTab AS StrTabMime,\n'
+ ' TestResults\n'
+ 'WHERE TestResultFiles.idTestResultFile = %s\n'
+ ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
+ ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
+ ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
+ ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
+ ' AND TestResults.idTestResult = TestResultFiles.idTestResult\n'
+ ' AND TestResults.idTestSet = %s\n'
+ , ( idTestResultFile, idTestSet, ));
+ return TestResultFileDataEx().initFromDbRow(self._oDb.fetchOne());
+
+
+ def getById(self, idTestSet):
+ """
+ Get TestSet table record by its id
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestSets\n'
+ 'WHERE idTestSet=%s\n',
+ (idTestSet,))
+
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise TMTooManyRows('Found more than one test sets with the same credentials. Database structure is corrupted.')
+ try:
+ return TestSetData().initFromDbRow(aRows[0])
+ except IndexError:
+ return None
+
+
+ def fetchOrphaned(self):
+ """
+ Returns a list of TestSetData objects of orphaned test sets.
+
+ A test set is orphaned if tsDone is NULL and the testbox has created
+ one or more newer testsets.
+ """
+
+ self._oDb.execute('SELECT TestSets.*\n'
+ 'FROM TestSets,\n'
+ ' (SELECT idTestSet, idTestBox FROM TestSets WHERE tsDone is NULL) AS t\n'
+ 'WHERE TestSets.idTestSet = t.idTestSet\n'
+ ' AND EXISTS(SELECT 1 FROM TestSets st\n'
+ ' WHERE st.idTestBox = t.idTestBox AND st.idTestSet > t.idTestSet)\n'
+ ' AND NOT EXISTS(SELECT 1 FROM TestBoxStatuses tbs\n'
+ ' WHERE tbs.idTestBox = t.idTestBox AND tbs.idTestSet = t.idTestSet)\n'
+ 'ORDER by TestSets.idTestBox, TestSets.idTestSet'
+ );
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(TestSetData().initFromDbRow(aoRow));
+ return aoRet;
+
+ def fetchByAge(self, tsNow = None, cHoursBack = 24):
+ """
+ Returns a list of TestSetData objects of a given time period (default is 24 hours).
+
+ Returns None if no testsets stored,
+ Returns an empty list if no testsets found with given criteria.
+ """
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+
+ if self._oDb.getRowCount() == 0:
+ return None;
+
+ self._oDb.execute('(SELECT *\n'
+ ' FROM TestSets\n'
+ ' WHERE tsDone <= %s\n'
+ ' AND tsDone > (%s - interval \'%s hours\')\n'
+ ')\n'
+ , ( tsNow, tsNow, cHoursBack, ));
+
+ aoRet = [];
+ for aoRow in self._oDb.fetchAll():
+ aoRet.append(TestSetData().initFromDbRow(aoRow));
+ return aoRet;
+
+ def isTestBoxExecutingTooRapidly(self, idTestBox): ## s/To/Too/
+ """
+ Checks whether the specified test box is executing tests too rapidly.
+
+ The parameters defining too rapid execution are defined in config.py.
+
+ Returns True if it does, False if it doesn't.
+ May raise database problems.
+ """
+
+ self._oDb.execute('(\n'
+ 'SELECT tsCreated\n'
+ 'FROM TestSets\n'
+ 'WHERE idTestBox = %s\n'
+ ' AND tsCreated >= (CURRENT_TIMESTAMP - interval \'%s seconds\')\n'
+ ') UNION (\n'
+ 'SELECT tsCreated\n'
+ 'FROM TestSets\n'
+ 'WHERE idTestBox = %s\n'
+ ' AND tsCreated >= (CURRENT_TIMESTAMP - interval \'%s seconds\')\n'
+ ' AND enmStatus >= \'failure\'\n'
+ ')'
+ , ( idTestBox, config.g_kcSecMinSinceLastTask,
+ idTestBox, config.g_kcSecMinSinceLastFailedTask, ));
+ return self._oDb.getRowCount() > 0;
+
+
+ #
+ # The virtual test sheriff interface.
+ #
+
+ def fetchBadTestBoxIds(self, cHoursBack = 2, tsNow = None, aidFailureReasons = None):
+ """
+ Fetches a list of test box IDs which returned bad-testbox statuses in the
+ given period (tsDone).
+ """
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+ if aidFailureReasons is None:
+ aidFailureReasons = [ -1, ];
+ self._oDb.execute('(SELECT idTestBox\n'
+ ' FROM TestSets\n'
+ ' WHERE TestSets.enmStatus = \'bad-testbox\'\n'
+ ' AND tsDone <= %s\n'
+ ' AND tsDone > (%s - interval \'%s hours\')\n'
+ ') UNION (\n'
+ ' SELECT TestSets.idTestBox\n'
+ ' FROM TestSets,\n'
+ ' TestResultFailures\n'
+ ' WHERE TestSets.tsDone <= %s\n'
+ ' AND TestSets.tsDone > (%s - interval \'%s hours\')\n'
+ ' AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n'
+ ' AND TestSets.idTestSet = TestResultFailures.idTestSet\n'
+ ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND TestResultFailures.idFailureReason IN ('
+ + ', '.join([str(i) for i in aidFailureReasons]) + ')\n'
+ ')\n'
+ , ( tsNow, tsNow, cHoursBack,
+ tsNow, tsNow, cHoursBack, ));
+ return [aoRow[0] for aoRow in self._oDb.fetchAll()];
+
+ def fetchSetsForTestBox(self, idTestBox, cHoursBack = 2, tsNow = None):
+ """
+ Fetches the TestSet rows for idTestBox for the given period (tsDone), w/o running ones.
+
+ Returns list of TestSetData sorted by tsDone in descending order.
+ """
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('SELECT *\n'
+ 'FROM TestSets\n'
+ 'WHERE TestSets.idTestBox = %s\n'
+ ' AND tsDone IS NOT NULL\n'
+ ' AND tsDone <= %s\n'
+ ' AND tsDone > (%s - interval \'%s hours\')\n'
+ 'ORDER by tsDone DESC\n'
+ , ( idTestBox, tsNow, tsNow, cHoursBack,));
+ return self._dbRowsToModelDataList(TestSetData);
+
+ def fetchFailedSetsWithoutReason(self, cHoursBack = 2, tsNow = None):
+ """
+ Fetches the TestSet failure rows without any currently (CURRENT_TIMESTAMP
+ not tsNow) assigned failure reason.
+
+ Returns list of TestSetData sorted by tsDone in descending order.
+
+ Note! Includes bad-testbox sets too as it can be useful to analyze these
+ too even if we normally count them in the 'skipped' category.
+ """
+ if tsNow is None:
+ tsNow = self._oDb.getCurrentTimestamp();
+ self._oDb.execute('SELECT TestSets.*\n'
+ 'FROM TestSets\n'
+ ' LEFT OUTER JOIN TestResultFailures\n'
+ ' ON TestResultFailures.idTestSet = TestSets.idTestSet\n'
+ ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'WHERE TestSets.tsDone IS NOT NULL\n'
+ ' AND TestSets.enmStatus IN ( %s, %s, %s, %s )\n'
+ ' AND TestSets.tsDone <= %s\n'
+ ' AND TestSets.tsDone > (%s - interval \'%s hours\')\n'
+ ' AND TestResultFailures.idTestSet IS NULL\n'
+ 'ORDER by tsDone DESC\n'
+ , ( TestSetData.ksTestStatus_Failure, TestSetData.ksTestStatus_TimedOut,
+ TestSetData.ksTestStatus_Rebooted, TestSetData.ksTestStatus_BadTestBox,
+ tsNow,
+ tsNow, cHoursBack,));
+ return self._dbRowsToModelDataList(TestSetData);
+
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class TestSetDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [TestSetData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
diff --git a/src/VBox/ValidationKit/testmanager/core/useraccount.pgsql b/src/VBox/ValidationKit/testmanager/core/useraccount.pgsql
new file mode 100644
index 00000000..1abd8b71
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/useraccount.pgsql
@@ -0,0 +1,178 @@
+-- $Id: useraccount.pgsql $
+--- @file
+-- VBox Test Manager Database Stored Procedures - UserAccounts.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+\set ON_ERROR_STOP 1
+\connect testmanager;
+
+---
+-- Checks if the user name and login name are unique, ignoring a_uidIgnore.
+-- Raises exception if duplicates are found.
+--
+-- @internal
+--
+CREATE OR REPLACE FUNCTION UserAccountLogic_checkUniqueUser(a_sUsername TEXT, a_sLoginName TEXT, a_uidIgnore INTEGER)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cRows INTEGER;
+ BEGIN
+ -- sUserName
+ SELECT COUNT(*) INTO v_cRows
+ FROM Users
+ WHERE sUsername = a_sUsername
+ AND tsExpire = 'infinity'::TIMESTAMP
+ AND uid <> a_uidIgnore;
+ IF v_cRows <> 0 THEN
+ RAISE EXCEPTION 'Duplicate user name "%" (% times)', a_sUsername, v_cRows;
+ END IF;
+
+ -- sLoginName
+ SELECT COUNT(*) INTO v_cRows
+ FROM Users
+ WHERE sLoginName = a_sLoginName
+ AND tsExpire = 'infinity'::TIMESTAMP
+ AND uid <> a_uidIgnore;
+ IF v_cRows <> 0 THEN
+ RAISE EXCEPTION 'Duplicate login name "%" (% times)', a_sUsername, v_cRows;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+---
+-- Check that the user account exists.
+-- Raises exception if it doesn't.
+--
+-- @internal
+--
+CREATE OR REPLACE FUNCTION UserAccountLogic_checkExists(a_uid INTEGER) RETURNS VOID AS $$
+ DECLARE
+ v_cUpdatedRows INTEGER;
+ BEGIN
+ IF NOT EXISTS( SELECT *
+ FROM Users
+ WHERE uid = a_uid
+ AND tsExpire = 'infinity'::TIMESTAMP ) THEN
+ RAISE EXCEPTION 'User with ID % does not currently exist', a_uid;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+---
+-- Historize a row.
+-- @internal
+--
+CREATE OR REPLACE FUNCTION UserAccountLogic_historizeEntry(a_uid INTEGER, a_tsExpire TIMESTAMP WITH TIME ZONE)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cUpdatedRows INTEGER;
+ BEGIN
+ UPDATE Users
+ SET tsExpire = a_tsExpire
+ WHERE uid = a_uid
+ AND tsExpire = 'infinity'::TIMESTAMP;
+ GET DIAGNOSTICS v_cUpdatedRows = ROW_COUNT;
+ IF v_cUpdatedRows <> 1 THEN
+ IF v_cUpdatedRows = 0 THEN
+ RAISE EXCEPTION 'User with ID % does not currently exist', a_uid;
+ END IF;
+ RAISE EXCEPTION 'Integrity error in UserAccounts: % current rows with uid=%d', v_cUpdatedRows, a_uid;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+
+---
+-- Adds a new user.
+--
+CREATE OR REPLACE FUNCTION UserAccountLogic_addEntry(a_uidAuthor INTEGER, a_sUsername TEXT, a_sEmail TEXT, a_sFullName TEXT,
+ a_sLoginName TEXT, a_fReadOnly BOOLEAN)
+ RETURNS VOID AS $$
+ DECLARE
+ v_cRows INTEGER;
+ BEGIN
+ PERFORM UserAccountLogic_checkUniqueUser(a_sUsername, a_sLoginName, -1);
+ INSERT INTO Users(uidAuthor, sUsername, sEmail, sFullName, sLoginName)
+ VALUES (a_uidAuthor, a_sUsername, a_sEmail, a_sFullName, a_sLoginName);
+ END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION UserAccountLogic_editEntry(a_uidAuthor INTEGER, a_uid INTEGER, a_sUsername TEXT, a_sEmail TEXT,
+ a_sFullName TEXT, a_sLoginName TEXT, a_fReadOnly BOOLEAN)
+ RETURNS VOID AS $$
+ BEGIN
+ PERFORM UserAccountLogic_checkExists(a_uid);
+ PERFORM UserAccountLogic_checkUniqueUser(a_sUsername, a_sLoginName, a_uid);
+
+ PERFORM UserAccountLogic_historizeEntry(a_uid, CURRENT_TIMESTAMP);
+ INSERT INTO Users (uid, uidAuthor, sUsername, sEmail, sFullName, sLoginName, fReadOnly)
+ VALUES (a_uid, a_uidAuthor, a_sUsername, a_sEmail, a_sFullName, a_sLoginName, a_fReadOnly);
+ END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION UserAccountLogic_delEntry(a_uidAuthor INTEGER, a_uid INTEGER) RETURNS VOID AS $$
+ DECLARE
+ v_Row Users%ROWTYPE;
+ v_tsEffective TIMESTAMP WITH TIME ZONE;
+ BEGIN
+ --
+ -- To preserve the information about who deleted the record, we try to
+ -- add a dummy record which expires immediately. I say try because of
+ -- the primary key, we must let the new record be valid for 1 us. :-(
+ --
+
+ SELECT * INTO STRICT v_Row
+ FROM Users
+ WHERE uid = a_uid
+ AND tsExpire = 'infinity'::TIMESTAMP;
+
+ v_tsEffective := CURRENT_TIMESTAMP - INTERVAL '1 microsecond';
+ IF v_Row.tsEffective < v_tsEffective THEN
+ PERFORM UserAccountLogic_historizeEntry(a_uid, v_tsEffective);
+ v_Row.tsEffective = v_tsEffective;
+ v_Row.tsExpire = CURRENT_TIMESTAMP;
+ INSERT INTO Users VALUES (v_Row.*);
+ ELSE
+ PERFORM UserAccountLogic_historizeEntry(a_uid, CURRENT_TIMESTAMP);
+ END IF;
+
+ EXCEPTION
+ WHEN NO_DATA_FOUND THEN
+ RAISE EXCEPTION 'User with ID % does not currently exist', a_uid;
+ WHEN TOO_MANY_ROWS THEN
+ RAISE EXCEPTION 'Integrity error in UserAccounts: Too many current rows for %', a_uid;
+ END;
+$$ LANGUAGE plpgsql;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/useraccount.py b/src/VBox/ValidationKit/testmanager/core/useraccount.py
new file mode 100755
index 00000000..6c3f3f51
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/useraccount.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8 -*-
+# $Id: useraccount.py $
+
+"""
+Test Manager - User DB records management.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMTooManyRows, TMRowNotFound;
+
+
+class UserAccountData(ModelDataBase):
+ """
+ User account data
+ """
+
+ ksIdAttr = 'uid';
+
+ ksParam_uid = 'UserAccount_uid'
+ ksParam_tsExpire = 'UserAccount_tsExpire'
+ ksParam_tsEffective = 'UserAccount_tsEffective'
+ ksParam_uidAuthor = 'UserAccount_uidAuthor'
+ ksParam_sLoginName = 'UserAccount_sLoginName'
+ ksParam_sUsername = 'UserAccount_sUsername'
+ ksParam_sEmail = 'UserAccount_sEmail'
+ ksParam_sFullName = 'UserAccount_sFullName'
+ ksParam_fReadOnly = 'UserAccount_fReadOnly'
+
+ kasAllowNullAttributes = ['uid', 'tsEffective', 'tsExpire', 'uidAuthor'];
+
+
+ def __init__(self):
+ """Init parameters"""
+ ModelDataBase.__init__(self);
+ self.uid = None;
+ self.tsEffective = None;
+ self.tsExpire = None;
+ self.uidAuthor = None;
+ self.sUsername = None;
+ self.sEmail = None;
+ self.sFullName = None;
+ self.sLoginName = None;
+ self.fReadOnly = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Init from database table row
+ Returns self. Raises exception of the row is None.
+ """
+ if aoRow is None:
+ raise TMRowNotFound('User not found.');
+
+ self.uid = aoRow[0];
+ self.tsEffective = aoRow[1];
+ self.tsExpire = aoRow[2];
+ self.uidAuthor = aoRow[3];
+ self.sUsername = aoRow[4];
+ self.sEmail = aoRow[5];
+ self.sFullName = aoRow[6];
+ self.sLoginName = aoRow[7];
+ self.fReadOnly = aoRow[8];
+ return self;
+
+ def initFromDbWithId(self, oDb, uid, tsNow = None, sPeriodBack = None):
+ """
+ Initialize the object from the database.
+ """
+ oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
+ 'SELECT *\n'
+ 'FROM Users\n'
+ 'WHERE uid = %s\n'
+ , ( uid, ), tsNow, sPeriodBack));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMRowNotFound('uid=%s not found (tsNow=%s sPeriodBack=%s)' % (uid, tsNow, sPeriodBack,));
+ return self.initFromDbRow(aoRow);
+
+ def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
+ # Custom handling of the email field.
+ if sAttr == 'sEmail':
+ return ModelDataBase.validateEmail(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
+
+ # Automatically lowercase the login name if we're supposed to do case
+ # insensitive matching. (The feature assumes lower case in DB.)
+ if sAttr == 'sLoginName' and oValue is not None and config.g_kfLoginNameCaseInsensitive:
+ oValue = oValue.lower();
+
+ return ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
+
+
+class UserAccountLogic(ModelLogicBase):
+ """
+ User account logic (for the Users table).
+ """
+
+ def __init__(self, oDb):
+ ModelLogicBase.__init__(self, oDb)
+ self.dCache = None;
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches user accounts.
+
+ Returns an array (list) of UserAccountData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = aiSortColumns;
+ if tsNow is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM Users\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ 'ORDER BY sUsername DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+ else:
+ self._oDb.execute('SELECT *\n'
+ 'FROM Users\n'
+ 'WHERE tsExpire > %s\n'
+ ' AND tsEffective <= %s\n'
+ 'ORDER BY sUsername DESC\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (tsNow, tsNow, cMaxRows, iStart,));
+
+ aoRows = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRows.append(UserAccountData().initFromDbRow(self._oDb.fetchOne()));
+ return aoRows;
+
+ def addEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Add user account entry to the DB.
+ """
+ self._oDb.callProc('UserAccountLogic_addEntry',
+ (uidAuthor, oData.sUsername, oData.sEmail, oData.sFullName, oData.sLoginName, oData.fReadOnly));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def editEntry(self, oData, uidAuthor, fCommit = False):
+ """
+ Modify user account.
+ """
+ self._oDb.callProc('UserAccountLogic_editEntry',
+ ( uidAuthor, oData.uid, oData.sUsername, oData.sEmail,
+ oData.sFullName, oData.sLoginName, oData.fReadOnly));
+ self._oDb.maybeCommit(fCommit);
+ return True;
+
+ def removeEntry(self, uidAuthor, uid, fCascade = False, fCommit = False):
+ """
+ Delete user account
+ """
+ self._oDb.callProc('UserAccountLogic_delEntry', (uidAuthor, uid));
+ self._oDb.maybeCommit(fCommit);
+ _ = fCascade;
+ return True;
+
+ def _getByField(self, sField, sValue):
+ """
+ Get user account record by its field value
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM Users\n'
+ 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
+ ' AND ' + sField + ' = %s'
+ , (sValue,))
+
+ aRows = self._oDb.fetchAll()
+ if len(aRows) not in (0, 1):
+ raise TMTooManyRows('Found more than one user account with the same credentials. Database structure is corrupted.')
+
+ try:
+ return aRows[0]
+ except IndexError:
+ return []
+
+ def getById(self, idUserId):
+ """
+ Get user account information by ID.
+ """
+ return self._getByField('uid', idUserId)
+
+ def tryFetchAccountByLoginName(self, sLoginName):
+ """
+ Try get user account information by login name.
+
+ Returns UserAccountData if found, None if not.
+ Raises exception on DB error.
+ """
+ if config.g_kfLoginNameCaseInsensitive:
+ sLoginName = sLoginName.lower();
+
+ self._oDb.execute('SELECT *\n'
+ 'FROM Users\n'
+ 'WHERE sLoginName = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (sLoginName, ));
+ if self._oDb.getRowCount() != 1:
+ if self._oDb.getRowCount() != 0:
+ raise self._oDb.integrityException('%u rows in Users with sLoginName="%s"'
+ % (self._oDb.getRowCount(), sLoginName));
+ return None;
+ return UserAccountData().initFromDbRow(self._oDb.fetchOne());
+
+ def cachedLookup(self, uid):
+ """
+ Looks up the current UserAccountData object for uid via an object cache.
+
+ Returns a shared UserAccountData object. None if not found.
+ Raises exception on DB error.
+ """
+ if self.dCache is None:
+ self.dCache = self._oDb.getCache('UserAccount');
+
+ oUser = self.dCache.get(uid, None);
+ if oUser is None:
+ self._oDb.execute('SELECT *\n'
+ 'FROM Users\n'
+ 'WHERE uid = %s\n'
+ ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
+ , (uid, ));
+ if self._oDb.getRowCount() == 0:
+ # Maybe it was deleted, try get the last entry.
+ self._oDb.execute('SELECT *\n'
+ 'FROM Users\n'
+ 'WHERE uid = %s\n'
+ 'ORDER BY tsExpire DESC\n'
+ 'LIMIT 1\n'
+ , (uid, ));
+ elif self._oDb.getRowCount() > 1:
+ raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), uid));
+
+ if self._oDb.getRowCount() == 1:
+ oUser = UserAccountData().initFromDbRow(self._oDb.fetchOne());
+ self.dCache[uid] = oUser;
+ return oUser;
+
+ def resolveChangeLogAuthors(self, aoEntries):
+ """
+ Given an array of ChangeLogEntry instances, set sAuthor to whatever
+ uidAuthor resolves to.
+
+ Returns aoEntries.
+ Raises exception on DB error.
+ """
+ for oEntry in aoEntries:
+ oUser = self.cachedLookup(oEntry.uidAuthor)
+ if oUser is not None:
+ oEntry.sAuthor = oUser.sUsername;
+ return aoEntries;
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class UserAccountDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [UserAccountData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/vcsbugreference.py b/src/VBox/ValidationKit/testmanager/core/vcsbugreference.py
new file mode 100755
index 00000000..9ae9c941
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/vcsbugreference.py
@@ -0,0 +1,251 @@
+# -*- coding: utf-8 -*-
+# $Id: vcsbugreference.py $
+
+"""
+Test Manager - VcsBugReferences
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase;
+
+
+class VcsBugReferenceData(ModelDataBase):
+ """
+ A version control system (VCS) bug tracker reference (commit message tag).
+ """
+
+ #kasIdAttr = ['sRepository','iRevision', 'sBugTracker', 'iBugNo'];
+
+ ksParam_sRepository = 'VcsBugReference_sRepository';
+ ksParam_iRevision = 'VcsBugReference_iRevision';
+ ksParam_sBugTracker = 'VcsBugReference_sBugTracker';
+ ksParam_lBugNo = 'VcsBugReference_lBugNo';
+
+ kasAllowNullAttributes = [ ];
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.sRepository = None;
+ self.iRevision = None;
+ self.sBugTracker = None;
+ self.lBugNo = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the object from a SELECT * FROM VcsBugReferences row.
+ Returns self. Raises exception if aoRow is None.
+ """
+ if aoRow is None:
+ raise TMExceptionBase('VcsBugReference not found.');
+
+ self.sRepository = aoRow[0];
+ self.iRevision = aoRow[1];
+ self.sBugTracker = aoRow[2];
+ self.lBugNo = aoRow[3];
+ return self;
+
+ def initFromValues(self, sRepository, iRevision, sBugTracker, lBugNo):
+ """
+ Reinitializes form a set of values.
+ return self.
+ """
+ self.sRepository = sRepository;
+ self.iRevision = iRevision;
+ self.sBugTracker = sBugTracker;
+ self.lBugNo = lBugNo;
+ return self;
+
+
+class VcsBugReferenceDataEx(VcsBugReferenceData):
+ """
+ Extended version of VcsBugReferenceData that includes the commit details.
+ """
+ def __init__(self):
+ VcsBugReferenceData.__init__(self);
+ self.tsCreated = None;
+ self.sAuthor = None;
+ self.sMessage = None;
+
+ def initFromDbRow(self, aoRow):
+ VcsBugReferenceData.initFromDbRow(self, aoRow);
+ self.tsCreated = aoRow[4];
+ self.sAuthor = aoRow[5];
+ self.sMessage = aoRow[6];
+ return self;
+
+
+class VcsBugReferenceLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ VCS revision <-> bug tracker references database logic.
+ """
+
+ #
+ # Standard methods.
+ #
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches VCS revisions for listing.
+
+ Returns an array (list) of VcsBugReferenceData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = tsNow; _ = aiSortColumns;
+ self._oDb.execute('''
+SELECT *
+FROM VcsBugReferences
+ORDER BY sRepository, iRevision, sBugTracker, lBugNo
+LIMIT %s OFFSET %s
+''', (cMaxRows, iStart,));
+
+ aoRows = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRows.append(VcsBugReferenceData().initFromDbRow(self._oDb.fetchOne()));
+ return aoRows;
+
+ def exists(self, oData):
+ """
+ Checks if the data is already present in the DB.
+ Returns True / False.
+ Raises exception on input and database errors.
+ """
+ self._oDb.execute('''
+SELECT COUNT(*)
+FROM VcsBugReferences
+WHERE sRepository = %s
+ AND iRevision = %s
+ AND sBugTracker = %s
+ AND lBugNo = %s
+''', ( oData.sRepository, oData.iRevision, oData.sBugTracker, oData.lBugNo));
+ cRows = self._oDb.fetchOne()[0];
+ if cRows < 0 or cRows > 1:
+ raise TMExceptionBase('VcsBugReferences has a primary key problem: %u duplicates' % (cRows,));
+ return cRows != 0;
+
+
+ #
+ # Other methods.
+ #
+
+ def addVcsBugReference(self, oData, fCommit = False):
+ """
+ Adds (or updates) a tree revision record.
+ Raises exception on input and database errors.
+ """
+
+ # Check VcsBugReferenceData before do anything
+ dDataErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dDataErrors:
+ raise TMExceptionBase('Invalid data passed to addVcsBugReference(): %s' % (dDataErrors,));
+
+ # Does it already exist?
+ if not self.exists(oData):
+ # New row.
+ self._oDb.execute('INSERT INTO VcsBugReferences (sRepository, iRevision, sBugTracker, lBugNo)\n'
+ 'VALUES (%s, %s, %s, %s)\n'
+ , ( oData.sRepository,
+ oData.iRevision,
+ oData.sBugTracker,
+ oData.lBugNo,
+ ));
+
+ self._oDb.maybeCommit(fCommit);
+ return oData;
+
+ def getLastRevision(self, sRepository):
+ """
+ Get the last known revision number for the given repository, returns 0
+ if the repository is not known to us:
+ """
+ self._oDb.execute('''
+SELECT iRevision
+FROM VcsBugReferences
+WHERE sRepository = %s
+ORDER BY iRevision DESC
+LIMIT 1
+''', ( sRepository, ));
+ if self._oDb.getRowCount() == 0:
+ return 0;
+ return self._oDb.fetchOne()[0];
+
+ def fetchForBug(self, sBugTracker, lBugNo):
+ """
+ Fetches VCS revisions for a bug.
+
+ Returns an array (list) of VcsBugReferenceDataEx items, empty list if none.
+ Raises exception on error.
+ """
+ self._oDb.execute('''
+SELECT VcsBugReferences.*,
+ VcsRevisions.tsCreated,
+ VcsRevisions.sAuthor,
+ VcsRevisions.sMessage
+FROM VcsBugReferences
+LEFT OUTER JOIN VcsRevisions ON ( VcsRevisions.sRepository = VcsBugReferences.sRepository
+ AND VcsRevisions.iRevision = VcsBugReferences.iRevision )
+WHERE sBugTracker = %s
+ AND lBugNo = %s
+ORDER BY VcsRevisions.tsCreated, VcsBugReferences.sRepository, VcsBugReferences.iRevision
+''', (sBugTracker, lBugNo,));
+
+ aoRows = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRows.append(VcsBugReferenceDataEx().initFromDbRow(self._oDb.fetchOne()));
+ return aoRows;
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class VcsBugReferenceDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [VcsBugReferenceData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/vcsrevisions.py b/src/VBox/ValidationKit/testmanager/core/vcsrevisions.py
new file mode 100755
index 00000000..ea841d11
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/vcsrevisions.py
@@ -0,0 +1,254 @@
+# -*- coding: utf-8 -*-
+# $Id: vcsrevisions.py $
+
+"""
+Test Manager - VcsRevisions
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import unittest;
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase;
+
+
+class VcsRevisionData(ModelDataBase):
+ """
+ A version control system (VCS) revision.
+ """
+
+ #kasIdAttr = ['sRepository',iRevision];
+
+ ksParam_sRepository = 'VcsRevision_sRepository';
+ ksParam_iRevision = 'VcsRevision_iRevision';
+ ksParam_tsCreated = 'VcsRevision_tsCreated';
+ ksParam_sAuthor = 'VcsRevision_sAuthor';
+ ksParam_sMessage = 'VcsRevision_sMessage';
+
+ kasAllowNullAttributes = [ ];
+ kfAllowUnicode_sMessage = True;
+ kcchMax_sMessage = 8192;
+
+ def __init__(self):
+ ModelDataBase.__init__(self);
+
+ #
+ # Initialize with defaults.
+ # See the database for explanations of each of these fields.
+ #
+ self.sRepository = None;
+ self.iRevision = None;
+ self.tsCreated = None;
+ self.sAuthor = None;
+ self.sMessage = None;
+
+ def initFromDbRow(self, aoRow):
+ """
+ Re-initializes the object from a SELECT * FROM VcsRevisions row.
+ Returns self. Raises exception if aoRow is None.
+ """
+ if aoRow is None:
+ raise TMExceptionBase('VcsRevision not found.');
+
+ self.sRepository = aoRow[0];
+ self.iRevision = aoRow[1];
+ self.tsCreated = aoRow[2];
+ self.sAuthor = aoRow[3];
+ self.sMessage = aoRow[4];
+ return self;
+
+ def initFromDbWithRepoAndRev(self, oDb, sRepository, iRevision):
+ """
+ Initialize from the database, given the tree and revision of a row.
+ """
+ oDb.execute('SELECT * FROM VcsRevisions WHERE sRepository = %s AND iRevision = %u', (sRepository, iRevision,));
+ aoRow = oDb.fetchOne()
+ if aoRow is None:
+ raise TMExceptionBase('sRepository = %s iRevision = %u not found' % (sRepository, iRevision, ));
+ return self.initFromDbRow(aoRow);
+
+ def initFromValues(self, sRepository, iRevision, tsCreated, sAuthor, sMessage):
+ """
+ Reinitializes form a set of values.
+ return self.
+ """
+ self.sRepository = sRepository;
+ self.iRevision = iRevision;
+ self.tsCreated = tsCreated;
+ self.sAuthor = sAuthor;
+ self.sMessage = sMessage;
+ return self;
+
+
+class VcsRevisionLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
+ """
+ VCS revisions database logic.
+ """
+
+ #
+ # Standard methods.
+ #
+
+ def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
+ """
+ Fetches VCS revisions for listing.
+
+ Returns an array (list) of VcsRevisionData items, empty list if none.
+ Raises exception on error.
+ """
+ _ = tsNow; _ = aiSortColumns;
+ self._oDb.execute('SELECT *\n'
+ 'FROM VcsRevisions\n'
+ 'ORDER BY tsCreated, sRepository, iRevision\n'
+ 'LIMIT %s OFFSET %s\n'
+ , (cMaxRows, iStart,));
+
+ aoRows = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRows.append(VcsRevisionData().initFromDbRow(self._oDb.fetchOne()));
+ return aoRows;
+
+ def tryFetch(self, sRepository, iRevision):
+ """
+ Tries to fetch the specified tree revision record.
+ Returns VcsRevisionData instance if found, None if not found.
+ Raises exception on input and database errors.
+ """
+ self._oDb.execute('SELECT * FROM VcsRevisions WHERE sRepository = %s AND iRevision = %s',
+ ( sRepository, iRevision, ));
+ aaoRows = self._oDb.fetchAll();
+ if len(aaoRows) == 1:
+ return VcsRevisionData().initFromDbRow(aaoRows[0]);
+ if aaoRows:
+ raise TMExceptionBase('VcsRevisions has a primary key problem: %u duplicates' % (len(aaoRows),));
+ return None
+
+
+ #
+ # Other methods.
+ #
+
+ def addVcsRevision(self, oData, fCommit = False):
+ """
+ Adds (or updates) a tree revision record.
+ Raises exception on input and database errors.
+ """
+
+ # Check VcsRevisionData before do anything
+ dDataErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
+ if dDataErrors:
+ raise TMExceptionBase('Invalid data passed to addVcsRevision(): %s' % (dDataErrors,));
+
+ # Does it already exist?
+ oOldData = self.tryFetch(oData.sRepository, oData.iRevision);
+ if oOldData is None:
+ # New row.
+ self._oDb.execute('INSERT INTO VcsRevisions (sRepository, iRevision, tsCreated, sAuthor, sMessage)\n'
+ 'VALUES (%s, %s, %s, %s, %s)\n'
+ , ( oData.sRepository,
+ oData.iRevision,
+ oData.tsCreated,
+ oData.sAuthor,
+ oData.sMessage,
+ ));
+ elif not oOldData.isEqual(oData):
+ # Update old row.
+ self._oDb.execute('UPDATE VcsRevisions\n'
+ ' SET tsCreated = %s,\n'
+ ' sAuthor = %s,\n'
+ ' sMessage = %s\n'
+ 'WHERE sRepository = %s\n'
+ ' AND iRevision = %s'
+ , ( oData.tsCreated,
+ oData.sAuthor,
+ oData.sMessage,
+ oData.sRepository,
+ oData.iRevision,
+ ));
+
+ self._oDb.maybeCommit(fCommit);
+ return oData;
+
+ def getLastRevision(self, sRepository):
+ """
+ Get the last known revision number for the given repository, returns 0
+ if the repository is not known to us:
+ """
+ self._oDb.execute('SELECT iRevision\n'
+ 'FROM VcsRevisions\n'
+ 'WHERE sRepository = %s\n'
+ 'ORDER BY iRevision DESC\n'
+ 'LIMIT 1\n'
+ , ( sRepository, ));
+ if self._oDb.getRowCount() == 0:
+ return 0;
+ return self._oDb.fetchOne()[0];
+
+ def fetchTimeline(self, sRepository, iRevision, cEntriesBack):
+ """
+ Fetches a VCS timeline portion for a repository.
+
+ Returns an array (list) of VcsRevisionData items, empty list if none.
+ Raises exception on error.
+ """
+ self._oDb.execute('SELECT *\n'
+ 'FROM VcsRevisions\n'
+ 'WHERE sRepository = %s\n'
+ ' AND iRevision > %s\n'
+ ' AND iRevision <= %s\n'
+ 'ORDER BY iRevision DESC\n'
+ 'LIMIT %s\n'
+ , ( sRepository, iRevision - cEntriesBack*2 + 1, iRevision, cEntriesBack));
+ aoRows = [];
+ for _ in range(self._oDb.getRowCount()):
+ aoRows.append(VcsRevisionData().initFromDbRow(self._oDb.fetchOne()));
+ return aoRows;
+
+
+#
+# Unit testing.
+#
+
+# pylint: disable=missing-docstring
+class VcsRevisionDataTestCase(ModelDataBaseTestCase):
+ def setUp(self):
+ self.aoSamples = [VcsRevisionData(),];
+
+if __name__ == '__main__':
+ unittest.main();
+ # not reached.
+
diff --git a/src/VBox/ValidationKit/testmanager/core/webservergluebase.py b/src/VBox/ValidationKit/testmanager/core/webservergluebase.py
new file mode 100755
index 00000000..a36e1af7
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/webservergluebase.py
@@ -0,0 +1,717 @@
+# -*- coding: utf-8 -*-
+# $Id: webservergluebase.py $
+
+"""
+Test Manager Core - Web Server Abstraction Base Class.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import cgitb
+import codecs;
+import os
+import sys
+
+# Validation Kit imports.
+from common import webutils, utils;
+from testmanager import config;
+
+
+class WebServerGlueException(Exception):
+ """
+ For exceptions raised by glue code.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class WebServerGlueBase(object):
+ """
+ Web server interface abstraction and some HTML utils.
+ """
+
+ ## Enables more debug output.
+ kfDebugInfoEnabled = True;
+
+ ## The maximum number of characters to cache.
+ kcchMaxCached = 65536;
+
+ ## Special getUserName return value.
+ ksUnknownUser = 'Unknown User';
+
+ ## HTTP status codes and their messages.
+ kdStatusMsgs = {
+ 100: 'Continue',
+ 101: 'Switching Protocols',
+ 102: 'Processing',
+ 103: 'Early Hints',
+ 200: 'OK',
+ 201: 'Created',
+ 202: 'Accepted',
+ 203: 'Non-Authoritative Information',
+ 204: 'No Content',
+ 205: 'Reset Content',
+ 206: 'Partial Content',
+ 207: 'Multi-Status',
+ 208: 'Already Reported',
+ 226: 'IM Used',
+ 300: 'Multiple Choices',
+ 301: 'Moved Permantently',
+ 302: 'Found',
+ 303: 'See Other',
+ 304: 'Not Modified',
+ 305: 'Use Proxy',
+ 306: 'Switch Proxy',
+ 307: 'Temporary Redirect',
+ 308: 'Permanent Redirect',
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 402: 'Payment Required',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 405: 'Method Not Allowed',
+ 406: 'Not Acceptable',
+ 407: 'Proxy Authentication Required',
+ 408: 'Request Timeout',
+ 409: 'Conflict',
+ 410: 'Gone',
+ 411: 'Length Required',
+ 412: 'Precondition Failed',
+ 413: 'Payload Too Large',
+ 414: 'URI Too Long',
+ 415: 'Unsupported Media Type',
+ 416: 'Range Not Satisfiable',
+ 417: 'Expectation Failed',
+ 418: 'I\'m a teapot',
+ 421: 'Misdirection Request',
+ 422: 'Unprocessable Entity',
+ 423: 'Locked',
+ 424: 'Failed Dependency',
+ 425: 'Too Early',
+ 426: 'Upgrade Required',
+ 428: 'Precondition Required',
+ 429: 'Too Many Requests',
+ 431: 'Request Header Fields Too Large',
+ 451: 'Unavailable For Legal Reasons',
+ 500: 'Internal Server Error',
+ 501: 'Not Implemented',
+ 502: 'Bad Gateway',
+ 503: 'Service Unavailable',
+ 504: 'Gateway Timeout',
+ 505: 'HTTP Version Not Supported',
+ 506: 'Variant Also Negotiates',
+ 507: 'Insufficient Storage',
+ 508: 'Loop Detected',
+ 510: 'Not Extended',
+ 511: 'Network Authentication Required',
+ };
+
+
+ def __init__(self, sValidationKitDir, fHtmlDebugOutput = True):
+ self._sValidationKitDir = sValidationKitDir;
+
+ # Debug
+ self.tsStart = utils.timestampNano();
+ self._fHtmlDebugOutput = fHtmlDebugOutput; # For trace
+ self._oDbgFile = sys.stderr;
+ if config.g_ksSrvGlueDebugLogDst is not None and config.g_kfSrvGlueDebug is True:
+ self._oDbgFile = open(config.g_ksSrvGlueDebugLogDst, 'a'); # pylint: disable=consider-using-with,unspecified-encoding
+ if config.g_kfSrvGlueCgiDumpArgs:
+ self._oDbgFile.write('Arguments: %s\nEnvironment:\n' % (sys.argv,));
+ if config.g_kfSrvGlueCgiDumpEnv:
+ for sVar in sorted(os.environ):
+ self._oDbgFile.write(' %s=\'%s\' \\\n' % (sVar, os.environ[sVar],));
+
+ self._afnDebugInfo = [];
+
+ # HTTP header.
+ self._fHeaderWrittenOut = False;
+ self._dHeaderFields = \
+ { \
+ 'Content-Type': 'text/html; charset=utf-8',
+ };
+
+ # Body.
+ self._sBodyType = None;
+ self._dParams = {};
+ self._sHtmlBody = '';
+ self._cchCached = 0;
+ self._cchBodyWrittenOut = 0;
+
+ # Output.
+ if sys.version_info[0] >= 3:
+ self.oOutputRaw = sys.stdout.detach(); # pylint: disable=no-member
+ sys.stdout = None; # Prevents flush_std_files() from complaining on stderr during sys.exit().
+ else:
+ self.oOutputRaw = sys.stdout;
+ self.oOutputText = codecs.getwriter('utf-8')(self.oOutputRaw);
+
+
+ #
+ # Get stuff.
+ #
+
+ def getParameters(self):
+ """
+ Returns a dictionary with the query parameters.
+
+ The parameter name is the key, the values are given as lists. If a
+ parameter is given more than once, the value is appended to the
+ existing dictionary entry.
+ """
+ return {};
+
+ def getClientAddr(self):
+ """
+ Returns the client address, as a string.
+ """
+ raise WebServerGlueException('getClientAddr is not implemented');
+
+ def getMethod(self):
+ """
+ Gets the HTTP request method.
+ """
+ return 'POST';
+
+ def getLoginName(self):
+ """
+ Gets login name provided by Apache.
+ Returns kUnknownUser if not logged on.
+ """
+ return WebServerGlueBase.ksUnknownUser;
+
+ def getUrlScheme(self):
+ """
+ Gets scheme name (aka. access protocol) from request URL, i.e. 'http' or 'https'.
+ See also urlparse.scheme.
+ """
+ return 'http';
+
+ def getUrlNetLoc(self):
+ """
+ Gets the network location (server host name / ip) from the request URL.
+ See also urlparse.netloc.
+ """
+ raise WebServerGlueException('getUrlNetLoc is not implemented');
+
+ def getUrlPath(self):
+ """
+ Gets the hirarchical path (relative to server) from the request URL.
+ See also urlparse.path.
+ Note! This includes the leading slash.
+ """
+ raise WebServerGlueException('getUrlPath is not implemented');
+
+ def getUrlBasePath(self):
+ """
+ Gets the hirarchical base path (relative to server) from the request URL.
+ Note! This includes both a leading an trailing slash.
+ """
+ sPath = self.getUrlPath(); # virtual method # pylint: disable=assignment-from-no-return
+ iLastSlash = sPath.rfind('/');
+ if iLastSlash >= 0:
+ sPath = sPath[:iLastSlash];
+ sPath = sPath.rstrip('/');
+ return sPath + '/';
+
+ def getUrl(self):
+ """
+ Gets the URL being accessed, sans parameters.
+ For instance this will return, "http://localhost/testmanager/admin.cgi"
+ when "http://localhost/testmanager/admin.cgi?blah=blah" is being access.
+ """
+ return '%s://%s%s' % (self.getUrlScheme(), self.getUrlNetLoc(), self.getUrlPath());
+
+ def getBaseUrl(self):
+ """
+ Gets the base URL (with trailing slash).
+ For instance this will return, "http://localhost/testmanager/" when
+ "http://localhost/testmanager/admin.cgi?blah=blah" is being access.
+ """
+ return '%s://%s%s' % (self.getUrlScheme(), self.getUrlNetLoc(), self.getUrlBasePath());
+
+ def getUserAgent(self):
+ """
+ Gets the User-Agent field of the HTTP header, returning empty string
+ if not present.
+ """
+ return '';
+
+ def getContentType(self):
+ """
+ Gets the Content-Type field of the HTTP header, parsed into a type
+ string and a dictionary.
+ """
+ return ('text/html', {});
+
+ def getContentLength(self):
+ """
+ Gets the content length.
+ Returns int.
+ """
+ return 0;
+
+ def getBodyIoStream(self):
+ """
+ Returns file object for reading the HTML body.
+ """
+ raise WebServerGlueException('getUrlPath is not implemented');
+
+ def getBodyIoStreamBinary(self):
+ """
+ Returns file object for reading the binary HTML body.
+ """
+ raise WebServerGlueException('getBodyIoStreamBinary is not implemented');
+
+ #
+ # Output stuff.
+ #
+
+ def _writeHeader(self, sHeaderLine):
+ """
+ Worker function which child classes can override.
+ """
+ sys.stderr.write('_writeHeader: cch=%s "%s..."\n' % (len(sHeaderLine), sHeaderLine[0:10],))
+ self.oOutputText.write(sHeaderLine);
+ return True;
+
+ def flushHeader(self):
+ """
+ Flushes the HTTP header.
+ """
+ if self._fHeaderWrittenOut is False:
+ for sKey, sValue in self._dHeaderFields.items():
+ self._writeHeader('%s: %s\n' % (sKey, sValue,));
+ self._fHeaderWrittenOut = True;
+ self._writeHeader('\n'); # End of header indicator.
+ return None;
+
+ def setHeaderField(self, sField, sValue):
+ """
+ Sets a header field.
+ """
+ assert self._fHeaderWrittenOut is False;
+ self._dHeaderFields[sField] = sValue;
+ return True;
+
+ def setRedirect(self, sLocation, iCode = 302):
+ """
+ Sets up redirection of the page.
+ Raises an exception if called too late.
+ """
+ if self._fHeaderWrittenOut is True:
+ raise WebServerGlueException('setRedirect called after the header was written');
+ if iCode != 302:
+ raise WebServerGlueException('Redirection code %d is not supported' % (iCode,));
+
+ self.setHeaderField('Location', sLocation);
+ self.setHeaderField('Status', '302 Found');
+ return True;
+
+ def setStatus(self, iStatus, sMsg = None):
+ """ Sets the status code. """
+ if not sMsg:
+ sMsg = self.kdStatusMsgs[iStatus];
+ return self.setHeaderField('Status', '%u %s' % (iStatus, sMsg));
+
+ def setContentType(self, sType):
+ """ Sets the content type header field. """
+ return self.setHeaderField('Content-Type', sType);
+
+ def _writeWorker(self, sChunkOfHtml):
+ """
+ Worker function which child classes can override.
+ """
+ sys.stderr.write('_writeWorker: cch=%s "%s..."\n' % (len(sChunkOfHtml), sChunkOfHtml[0:10],))
+ self.oOutputText.write(sChunkOfHtml);
+ return True;
+
+ def write(self, sChunkOfHtml):
+ """
+ Writes chunk of HTML, making sure the HTTP header is flushed first.
+ """
+ if self._sBodyType is None:
+ self._sBodyType = 'html';
+ elif self._sBodyType != 'html':
+ raise WebServerGlueException('Cannot use writeParameter when body type is "%s"' % (self._sBodyType, ));
+
+ self._sHtmlBody += sChunkOfHtml;
+ self._cchCached += len(sChunkOfHtml);
+
+ if self._cchCached > self.kcchMaxCached:
+ self.flush();
+ return True;
+
+ def writeRaw(self, abChunk):
+ """
+ Writes a raw chunk the document. Can be binary or any encoding.
+ No caching.
+ """
+ if self._sBodyType is None:
+ self._sBodyType = 'raw';
+ elif self._sBodyType != 'raw':
+ raise WebServerGlueException('Cannot use writeRaw when body type is "%s"' % (self._sBodyType, ));
+
+ self.flushHeader();
+ if self._cchCached > 0:
+ self.flush();
+
+ sys.stderr.write('writeRaw: cb=%s\n' % (len(abChunk),))
+ self.oOutputRaw.write(abChunk);
+ return True;
+
+ def writeParams(self, dParams):
+ """
+ Writes one or more reply parameters in a form style response. The names
+ and values in dParams are unencoded, this method takes care of that.
+
+ Note! This automatically changes the content type to
+ 'application/x-www-form-urlencoded', if the header hasn't been flushed
+ already.
+ """
+ if self._sBodyType is None:
+ if not self._fHeaderWrittenOut:
+ self.setHeaderField('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');
+ elif self._dHeaderFields['Content-Type'] != 'application/x-www-form-urlencoded; charset=utf-8':
+ raise WebServerGlueException('Cannot use writeParams when content-type is "%s"' % \
+ (self._dHeaderFields['Content-Type'],));
+ self._sBodyType = 'form';
+
+ elif self._sBodyType != 'form':
+ raise WebServerGlueException('Cannot use writeParams when body type is "%s"' % (self._sBodyType, ));
+
+ for sKey in dParams:
+ sValue = str(dParams[sKey]);
+ self._dParams[sKey] = sValue;
+ self._cchCached += len(sKey) + len(sValue);
+
+ if self._cchCached > self.kcchMaxCached:
+ self.flush();
+
+ return True;
+
+ def flush(self):
+ """
+ Flush the output.
+ """
+ self.flushHeader();
+
+ if self._sBodyType == 'form':
+ sBody = webutils.encodeUrlParams(self._dParams);
+ self._writeWorker(sBody);
+
+ self._dParams = {};
+ self._cchBodyWrittenOut += self._cchCached;
+
+ elif self._sBodyType == 'html':
+ self._writeWorker(self._sHtmlBody);
+
+ self._sHtmlBody = '';
+ self._cchBodyWrittenOut += self._cchCached;
+
+ self._cchCached = 0;
+ return None;
+
+ #
+ # Paths.
+ #
+
+ def pathTmWebUI(self):
+ """
+ Gets the path to the TM 'webui' directory.
+ """
+ return os.path.join(self._sValidationKitDir, 'testmanager', 'webui');
+
+ #
+ # Error stuff & Debugging.
+ #
+
+ def errorLog(self, sError, aXcptInfo, sLogFile):
+ """
+ Writes the error to a log file.
+ """
+ # Easy solution for log file size: Only one report.
+ try: os.unlink(sLogFile);
+ except: pass;
+
+ # Try write the log file.
+ fRc = True;
+ fSaved = self._fHtmlDebugOutput;
+
+ try:
+ with open(sLogFile, 'w') as oFile: # pylint: disable=unspecified-encoding
+ oFile.write(sError + '\n\n');
+ if aXcptInfo[0] is not None:
+ oFile.write(' B a c k t r a c e\n');
+ oFile.write('===================\n');
+ oFile.write(cgitb.text(aXcptInfo, 5));
+ oFile.write('\n\n');
+
+ oFile.write(' D e b u g I n f o\n');
+ oFile.write('=====================\n\n');
+ self._fHtmlDebugOutput = False;
+ self.debugDumpStuff(oFile.write);
+ except:
+ fRc = False;
+
+ self._fHtmlDebugOutput = fSaved;
+ return fRc;
+
+ def errorPage(self, sError, aXcptInfo, sLogFile = None):
+ """
+ Displays a page with an error message.
+ """
+ if sLogFile is not None:
+ self.errorLog(sError, aXcptInfo, sLogFile);
+
+ # Reset buffering, hoping that nothing was flushed yet.
+ self._sBodyType = None;
+ self._sHtmlBody = '';
+ self._cchCached = 0;
+ if not self._fHeaderWrittenOut:
+ if self._fHtmlDebugOutput:
+ self.setHeaderField('Content-Type', 'text/html; charset=utf-8');
+ else:
+ self.setHeaderField('Content-Type', 'text/plain; charset=utf-8');
+
+ # Write the error page.
+ if self._fHtmlDebugOutput:
+ self.write('<html><head><title>Test Manage Error</title></head>\n' +
+ '<body><h1>Test Manager Error:</h1>\n' +
+ '<p>' + sError + '</p>\n');
+ else:
+ self.write(' Test Manage Error\n'
+ '===================\n'
+ '\n'
+ '' + sError + '\n\n');
+
+ if aXcptInfo[0] is not None:
+ if self._fHtmlDebugOutput:
+ self.write('<h1>Backtrace:</h1>\n');
+ self.write(cgitb.html(aXcptInfo, 5));
+ else:
+ self.write('Backtrace\n'
+ '---------\n'
+ '\n');
+ self.write(cgitb.text(aXcptInfo, 5));
+ self.write('\n\n');
+
+ if self.kfDebugInfoEnabled:
+ if self._fHtmlDebugOutput:
+ self.write('<h1>Debug Info:</h1>\n');
+ else:
+ self.write('Debug Info\n'
+ '----------\n'
+ '\n');
+ self.debugDumpStuff();
+
+ for fn in self._afnDebugInfo:
+ try:
+ fn(self, self._fHtmlDebugOutput);
+ except Exception as oXcpt:
+ self.write('\nDebug info callback %s raised exception: %s\n' % (fn, oXcpt));
+
+ if self._fHtmlDebugOutput:
+ self.write('</body></html>');
+
+ self.flush();
+
+ def debugInfoPage(self, fnWrite = None):
+ """
+ Dumps useful debug info.
+ """
+ if fnWrite is None:
+ fnWrite = self.write;
+
+ fnWrite('<html><head><title>Test Manage Debug Info</title></head>\n<body>\n');
+ self.debugDumpStuff(fnWrite = fnWrite);
+ fnWrite('</body></html>');
+ self.flush();
+
+ def debugDumpDict(self, sName, dDict, fSorted = True, fnWrite = None):
+ """
+ Dumps dictionary.
+ """
+ if fnWrite is None:
+ fnWrite = self.write;
+
+ asKeys = list(dDict.keys());
+ if fSorted:
+ asKeys.sort();
+
+ if self._fHtmlDebugOutput:
+ fnWrite('<h2>%s</h2>\n'
+ '<table border="1"><tr><th>name</th><th>value</th></tr>\n' % (sName,));
+ for sKey in asKeys:
+ fnWrite(' <tr><td>' + webutils.escapeElem(sKey) + '</td><td>' \
+ + webutils.escapeElem(str(dDict.get(sKey))) \
+ + '</td></tr>\n');
+ fnWrite('</table>\n');
+ else:
+ for i in range(len(sName) - 1):
+ fnWrite('%s ' % (sName[i],));
+ fnWrite('%s\n\n' % (sName[-1],));
+
+ fnWrite('%28s Value\n' % ('Name',));
+ fnWrite('------------------------------------------------------------------------\n');
+ for sKey in asKeys:
+ fnWrite('%28s: %s\n' % (sKey, dDict.get(sKey),));
+ fnWrite('\n');
+
+ return True;
+
+ def debugDumpList(self, sName, aoStuff, fnWrite = None):
+ """
+ Dumps array.
+ """
+ if fnWrite is None:
+ fnWrite = self.write;
+
+ if self._fHtmlDebugOutput:
+ fnWrite('<h2>%s</h2>\n'
+ '<table border="1"><tr><th>index</th><th>value</th></tr>\n' % (sName,));
+ for i, _ in enumerate(aoStuff):
+ fnWrite(' <tr><td>' + str(i) + '</td><td>' + webutils.escapeElem(str(aoStuff[i])) + '</td></tr>\n');
+ fnWrite('</table>\n');
+ else:
+ for ch in sName[:-1]:
+ fnWrite('%s ' % (ch,));
+ fnWrite('%s\n\n' % (sName[-1],));
+
+ fnWrite('Index Value\n');
+ fnWrite('------------------------------------------------------------------------\n');
+ for i, oStuff in enumerate(aoStuff):
+ fnWrite('%5u %s\n' % (i, str(oStuff)));
+ fnWrite('\n');
+
+ return True;
+
+ def debugDumpParameters(self, fnWrite):
+ """ Dumps request parameters. """
+ if fnWrite is None:
+ fnWrite = self.write;
+
+ try:
+ dParams = self.getParameters();
+ return self.debugDumpDict('Parameters', dParams);
+ except Exception as oXcpt:
+ if self._fHtmlDebugOutput:
+ fnWrite('<p>Exception %s while retriving parameters.</p>\n' % (oXcpt,))
+ else:
+ fnWrite('Exception %s while retriving parameters.\n' % (oXcpt,))
+ return False;
+
+ def debugDumpEnv(self, fnWrite = None):
+ """ Dumps os.environ. """
+ return self.debugDumpDict('Environment (os.environ)', os.environ, fnWrite = fnWrite);
+
+ def debugDumpArgv(self, fnWrite = None):
+ """ Dumps sys.argv. """
+ return self.debugDumpList('Arguments (sys.argv)', sys.argv, fnWrite = fnWrite);
+
+ def debugDumpPython(self, fnWrite = None):
+ """
+ Dump python info.
+ """
+ dInfo = {};
+ dInfo['sys.version'] = sys.version;
+ dInfo['sys.hexversion'] = sys.hexversion;
+ dInfo['sys.api_version'] = sys.api_version;
+ if hasattr(sys, 'subversion'):
+ dInfo['sys.subversion'] = sys.subversion; # pylint: disable=no-member
+ dInfo['sys.platform'] = sys.platform;
+ dInfo['sys.executable'] = sys.executable;
+ dInfo['sys.copyright'] = sys.copyright;
+ dInfo['sys.byteorder'] = sys.byteorder;
+ dInfo['sys.exec_prefix'] = sys.exec_prefix;
+ dInfo['sys.prefix'] = sys.prefix;
+ dInfo['sys.path'] = sys.path;
+ dInfo['sys.builtin_module_names'] = sys.builtin_module_names;
+ dInfo['sys.flags'] = sys.flags;
+
+ return self.debugDumpDict('Python Info', dInfo, fnWrite = fnWrite);
+
+
+ def debugDumpStuff(self, fnWrite = None):
+ """
+ Dumps stuff to the error page and debug info page.
+ Should be extended by child classes when possible.
+ """
+ self.debugDumpParameters(fnWrite);
+ self.debugDumpEnv(fnWrite);
+ self.debugDumpArgv(fnWrite);
+ self.debugDumpPython(fnWrite);
+ return True;
+
+ def dprint(self, sMessage):
+ """
+ Prints to debug log (usually apache error log).
+ """
+ if config.g_kfSrvGlueDebug is True:
+ if config.g_kfSrvGlueDebugTS is False:
+ self._oDbgFile.write(sMessage);
+ if not sMessage.endswith('\n'):
+ self._oDbgFile.write('\n');
+ else:
+ tsNow = utils.timestampMilli();
+ tsReq = tsNow - (self.tsStart / 1000000);
+ iPid = os.getpid();
+ for sLine in sMessage.split('\n'):
+ self._oDbgFile.write('%s/%03u,pid=%04x: %s\n' % (tsNow, tsReq, iPid, sLine,));
+
+ return True;
+
+ def registerDebugInfoCallback(self, fnDebugInfo):
+ """
+ Registers a debug info method for calling when the error page is shown.
+
+ The fnDebugInfo function takes two parameters. The first is this
+ object, the second is a boolean indicating html (True) or text (False)
+ output. The return value is ignored.
+ """
+ if self.kfDebugInfoEnabled:
+ self._afnDebugInfo.append(fnDebugInfo);
+ return True;
+
+ def unregisterDebugInfoCallback(self, fnDebugInfo):
+ """
+ Unregisters a debug info method previously registered by
+ registerDebugInfoCallback.
+ """
+ if self.kfDebugInfoEnabled:
+ try: self._afnDebugInfo.remove(fnDebugInfo);
+ except: pass;
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/core/webservergluecgi.py b/src/VBox/ValidationKit/testmanager/core/webservergluecgi.py
new file mode 100755
index 00000000..e530c7ec
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/core/webservergluecgi.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# $Id: webservergluecgi.py $
+
+"""
+Test Manager Core - Web Server Abstraction Base Class.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import cgitb;
+import os;
+import sys;
+import cgi;
+
+# Validation Kit imports.
+from testmanager.core.webservergluebase import WebServerGlueBase;
+from testmanager import config;
+
+
+class WebServerGlueCgi(WebServerGlueBase):
+ """
+ CGI glue.
+ """
+ def __init__(self, sValidationKitDir, fHtmlOutput=True):
+ WebServerGlueBase.__init__(self, sValidationKitDir, fHtmlOutput);
+
+ if config.g_kfSrvGlueCgiTb is True:
+ cgitb.enable(format=('html' if fHtmlOutput else 'text'));
+
+ def getParameters(self):
+ return cgi.parse(keep_blank_values=True);
+
+ def getClientAddr(self):
+ return os.environ.get('REMOTE_ADDR');
+
+ def getMethod(self):
+ return os.environ.get('REQUEST_METHOD', 'POST');
+
+ def getLoginName(self):
+ return os.environ.get('REMOTE_USER', WebServerGlueBase.ksUnknownUser);
+
+ def getUrlScheme(self):
+ return 'https' if 'HTTPS' in os.environ else 'http';
+
+ def getUrlNetLoc(self):
+ return os.environ['HTTP_HOST'];
+
+ def getUrlPath(self):
+ return os.environ['REQUEST_URI'];
+
+ def getUserAgent(self):
+ return os.getenv('HTTP_USER_AGENT', '');
+
+ def getContentType(self):
+ return cgi.parse_header(os.environ.get('CONTENT_TYPE', 'text/html'));
+
+ def getContentLength(self):
+ return int(os.environ.get('CONTENT_LENGTH', 0));
+
+ def getBodyIoStream(self):
+ return sys.stdin;
+
+ def getBodyIoStreamBinary(self):
+ # Python 3: sys.stdin.read() returns a string. To get untranslated
+ # binary data we use the sys.stdin.buffer object instead.
+ return getattr(sys.stdin, 'buffer', sys.stdin);
+
diff --git a/src/VBox/ValidationKit/testmanager/db/Makefile.kmk b/src/VBox/ValidationKit/testmanager/db/Makefile.kmk
new file mode 100644
index 00000000..194c01a2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/Makefile.kmk
@@ -0,0 +1,98 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit - Makefile for generating .html from .txt.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+
+# Need proper shell on windows.
+DEPTH = ../../../../..
+ifneq ($(wildcard $(DEPTH)/Config.kmk),)
+ include $(KBUILD_PATH)/header.kmk
+else
+ VBOX_BLD_PYTHON ?= python
+endif
+
+
+GENERATED_FILES = TestManagerDatabaseComments.pgsql
+PSQL := $(firstword $(which $(foreach pgver, 16 15 14 13 12 10 11 95 94 93 92,psql$(pgver)) ) psql)
+ifeq ($(PSQL_DB_HOST),)
+ PSQL_DB_HOST := localhost # Use localhost if nothing else is set.
+endif
+ifeq ($(PSQL_DB_PORT),)
+ PSQL_DB_PORT := 5432 # Same for the port; use the default.
+endif
+ifeq ($(PSQL_DB_USER),)
+ PSQL_DB_USER := postgres
+endif
+PSQL_OPTS = --user=$(PSQL_DB_USER) --set=ON_ERROR_STOP=1 --host=$(PSQL_DB_HOST) --port=$(PSQL_DB_PORT)
+
+all: $(GENERATED_FILES)
+
+clean:
+ kmk_builtin_rm -f -- $(GENERATED_FILES)
+
+
+TestManagerDatabaseComments.pgsql: TestManagerDatabaseInit.pgsql gen-sql-comments.py
+ LC_ALL=C $(VBOX_BLD_PYTHON) gen-sql-comments.py $< > $@
+
+
+load-testmanager-db: \
+ TestManagerDatabaseInit.pgsql \
+ TestManagerDatabaseComments.pgsql \
+ ../core/useraccount.pgsql \
+ ../core/testcase.pgsql \
+ ../core/testbox.pgsql \
+ ../core/globalresource.pgsql
+ @kmk_builtin_echo "Creating testmanager database: For script verification only!"
+ $(PSQL) $(PSQL_OPTS) -f TestManagerDatabaseInit.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f TestManagerDatabaseComments.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/useraccount.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/testcase.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/testbox.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/globalresource.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f TestManagerDatabaseDefaultUserAccounts.pgsql
+
+reload-testmanager-db-functions: \
+ ../core/useraccount.pgsql \
+ ../core/testcase.pgsql \
+ ../core/testbox.pgsql \
+ ../core/globalresource.pgsql
+ @kmk_builtin_echo "Reloading testmanager database functions"
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/useraccount.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/testcase.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/testbox.pgsql
+ $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/globalresource.pgsql
+
+# Only for prettier graphs:
+# $(PSQL) $(PSQL_OPTS) -d testmanager -f TestManagerDatabaseForeignKeyErHacks.pgsql
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase.dmd b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase.dmd
new file mode 100644
index 00000000..cf35f3fb
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase.dmd
@@ -0,0 +1,8 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<OSDM_Design class="oracle.dbtools.crest.model.design.Design" name="TestManagerDatabase" id="99299876-6D97-026B-55F9-DF582D334681" version="3.5">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 21:58:45 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<capitalNames>false</capitalNames>
+<designId>99299876-6D97-026B-55F9-DF582D334681</designId>
+</OSDM_Design> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/DataTypes.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/DataTypes.xml
new file mode 100644
index 00000000..9b86b6dd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/DataTypes.xml
@@ -0,0 +1,15 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<DataTypesDesign class="oracle.dbtools.crest.model.design.datatypes.DataTypesDesign" name="DataTypes" id="E0EE53BE-07B1-7CE9-B0DA-5D939EA4A3C9" mainViewID="E9476B45-3C62-EE27-4705-6F1EFAD11B74">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 21:58:45 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<shouldBeOpen>false</shouldBeOpen>
+<collectionOfRefsPrefix>array_ref_</collectionOfRefsPrefix>
+<collectionPrefix>array_</collectionPrefix>
+<defaultArrayLimit>10</defaultArrayLimit>
+<defaultCollectionType_Kind>ARRAY</defaultCollectionType_Kind>
+<defaultCollectionType_Suffix>_Array</defaultCollectionType_Suffix>
+<embeddedStructuredTypePrefix>inst_</embeddedStructuredTypePrefix>
+<referencePrefix>ref_</referencePrefix>
+<useRoleInAssociationEndAsName>true</useRoleInAssociationEndAsName>
+</DataTypesDesign> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/47E390DE-0671-C4B1-8428-0F45CBEE18F8.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/47E390DE-0671-C4B1-8428-0F45CBEE18F8.xml
new file mode 100644
index 00000000..e274e153
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/47E390DE-0671-C4B1-8428-0F45CBEE18F8.xml
@@ -0,0 +1,37 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<StructuredType class="oracle.dbtools.crest.model.design.datatypes.StructuredType" name="SDO_GEOMETRY" id="47E390DE-0671-C4B1-8428-0F45CBEE18F8" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 21:58:45 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<visible>false</visible>
+<predefined>true</predefined>
+<final>false</final>
+<instantiable>true</instantiable>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16777056</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Method</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Instantiable</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Mandatory</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+</fonts>
+</StructuredType> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1.xml
new file mode 100644
index 00000000..5ff7c301
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1.xml
@@ -0,0 +1,37 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<StructuredType class="oracle.dbtools.crest.model.design.datatypes.StructuredType" name="XMLTYPE" id="F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 21:58:45 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<visible>false</visible>
+<predefined>true</predefined>
+<final>false</final>
+<instantiable>true</instantiable>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16777056</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Method</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Instantiable</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Mandatory</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+</fonts>
+</StructuredType> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/subviews/E9476B45-3C62-EE27-4705-6F1EFAD11B74.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/subviews/E9476B45-3C62-EE27-4705-6F1EFAD11B74.xml
new file mode 100644
index 00000000..d09e8a61
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/subviews/E9476B45-3C62-EE27-4705-6F1EFAD11B74.xml
@@ -0,0 +1,21 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Diagram class="oracle.dbtools.crest.swingui.datatypes.DPVDataTypes" id="E9476B45-3C62-EE27-4705-6F1EFAD11B74">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 21:58:45 UTC</createdTime>
+<autoRoute>false</autoRoute>
+<boxInbox>true</boxInbox>
+<showLegend>false</showLegend>
+<showLabels>false</showLabels>
+<showGrid>false</showGrid>
+<diagramColor>-1</diagramColor>
+<display>false</display>
+<notation>0</notation>
+<objectViews>
+<OView class="oracle.dbtools.crest.swingui.datatypes.TVStructuredType" oid="47E390DE-0671-C4B1-8428-0F45CBEE18F8" otype="StructuredType" vid="48CB0B19-5276-2CC9-FC10-6C17D5E5FAC6">
+<bounds x="20" y="20" width="100" height="100"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.datatypes.TVStructuredType" oid="F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1" otype="StructuredType" vid="5CEA75E2-B53E-AD72-1C75-F8820307529C">
+<bounds x="20" y="20" width="100" height="100"/>
+</OView>
+</objectViews>
+</Diagram> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultRDBMSSites.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultRDBMSSites.xml
new file mode 100644
index 00000000..07122f85
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultRDBMSSites.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<sites version="1.0">
+ <site name="Oracle Database 11g" type="9" oid="32076570-2523-435C-2E92-BF29817DFF70" pathid="1" />
+ <site name="Oracle Database 10g" type="8" oid="D9582E4E-79E2-319F-387A-2ED963CB9D32" pathid="2" />
+ <site name="Oracle9i" type="7" oid="9807C1FA-0550-772D-1F14-16B19CA63681" pathid="3" />
+ <site name="SQL Server 2005" type="5" oid="B0943E51-0387-1F2A-CED9-5FB738BA5A0C" pathid="4" />
+ <site name="SQL Server 2000" type="4" oid="3424E3DB-6FE1-14EB-9311-F76EF3096E76" pathid="5" />
+ <site name="DB2/390 8" type="1" oid="CC7FDCE5-F5A5-F2C0-C9A7-0C07C92C898D" pathid="6" />
+ <site name="DB2/390 7" type="0" oid="26535E02-9B31-3EDE-24D5-4E3188C99288" pathid="7" />
+ <site name="DB2/UDB 8.1" type="3" oid="2BAE410E-5CEB-5134-8F33-CCB20E003569" pathid="8" />
+ <site name="DB2/UDB 7.1" type="2" oid="BA6252DC-29CE-184D-7701-48F55E3954D4" pathid="9" />
+</sites> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultdomains.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultdomains.xml
new file mode 100644
index 00000000..d858b5c4
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultdomains.xml
@@ -0,0 +1,13 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<DomainFile class="oracle.dbtools.crest.model.design.DomainFileWrapper" fileName="defaultdomains">
+ <domains>
+ <Domain class="oracle.dbtools.crest.model.design.Domain" name="Unknown" id="DOM3000004">
+ <createdBy>bird</createdBy>
+ <createdTime>2012-08-20 21:58:45 UTC</createdTime>
+ <ownerDesignName>System</ownerDesignName>
+ <avTSortOrder>0</avTSortOrder>
+ <fileName>defaultdomains</fileName>
+ <logicalDatatype>LOGDT017</logicalDatatype>
+ </Domain>
+ </domains>
+</DomainFile> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dl_settings.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dl_settings.xml
new file mode 100644
index 00000000..6c426008
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dl_settings.xml
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<settings>
+ <logical_type_for_domain_presentation value="false" />
+ <automatic_pk_generation value="false" />
+ <automatic_uk_generation value="false" />
+ <automatic_fk_generation value="false" />
+ <substitution_patterns>
+ </substitution_patterns>
+ <classification_types>
+ <type name="Fact" color="-7482" prefix="" id="1" />
+ <type name="Dimension" color="-1781507" prefix="" id="2" />
+ <type name="Logging" color="-1776412" prefix="" id="3" />
+ <type name="Summary" color="-3148598" prefix="" id="4" />
+ <type name="Temporary" color="-1" prefix="" id="5" />
+ </classification_types>
+ <default_fonts_and_colors>
+ <fc_object classname="Entity" background="-5971457" foreground="-16776961">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Attribute" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Datatype" font_color="-16744448" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="PK Element" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="FK Element" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="UK Element" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Not Null" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Key" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Logical View" background="-25750" foreground="-16776961">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Attribute" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Datatype" font_color="-16744448" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Table" background="-76" foreground="-16776961">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Column" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Datatype" font_color="-16744448" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="PK Element" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="FK Element" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="UK Element" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Not Null" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Key" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Relational View" background="-6881386" foreground="-16776961">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Column" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Datatype" font_color="-16744448" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Structured Type" background="-7537956" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Attribute" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Datatype" font_color="-16777056" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Method" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Not Instantiable" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Mandatory" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Cube" background="-7482" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Fact Entities" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Measure Type" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Measure" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Function" font_color="-16777056" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Formula" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Child to Parent Attributes" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Dimension" background="-16713196" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Level" background="-1781507" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Level Entity" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Type" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Attribute" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Function" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Process" background="-106" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Process Number" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Transformation Task" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="External Agent" background="-5570646" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Information Store" background="-10170881" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Number" font_color="-1" font_name="Dialog" font_size="10" font_style="1"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="In-Out Parameters" background="-328966" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16777216" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Parameters" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ <font_object fo_type="Datatype" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Transformation" background="-43" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16777216" font_name="Dialog" font_size="10" font_style="1"/>
+ <font_object fo_type="Process Number" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Note" background="-4144960" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Title" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Label" background="-1" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Text" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ <fc_object classname="Legend" background="-1" foreground="-16777216">
+ <fonts>
+ <font_object fo_type="Text" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/>
+ </fonts>
+ </fc_object>
+ </default_fonts_and_colors>
+ <default_line_widths_and_colors>
+ <lwc_object classname="Logical Relation" color="-16777216" width="1">
+ </lwc_object>
+ <lwc_object classname="Logical Inheritance" color="-65536" width="1">
+ </lwc_object>
+ <lwc_object classname="Relational Foreign Key" color="-16777216" width="1">
+ </lwc_object>
+ <lwc_object classname="Type Substitution" color="-16725996" width="1">
+ </lwc_object>
+ <lwc_object classname="Datatype Reference" color="-16776961" width="1">
+ </lwc_object>
+ <lwc_object classname="Datatype Inheritance" color="-65536" width="1">
+ </lwc_object>
+ <lwc_object classname="Multidimentional Link" color="-16776961" width="1">
+ </lwc_object>
+ <lwc_object classname="Multidimensional Hierarchy" color="-16725996" width="1">
+ </lwc_object>
+ <lwc_object classname="Process Flow" color="-65536" width="1">
+ </lwc_object>
+ </default_line_widths_and_colors>
+ <naming_standard_rules>
+ <logical>
+ <separator value= "Title Case" char=" "/>
+ <entity>
+ </entity>
+ <attribute>
+ </attribute>
+ </logical>
+ <relational>
+ <separator value= "_" abbreviated_only="false"/>
+ <table>
+ </table>
+ <column>
+ </column>
+ </relational>
+ <domains>
+ <separator value= " "/>
+ <domain>
+ </domain>
+ </domains>
+ <constraints>
+ <pk value="{table}_PK"/>
+ <fk value="{child}_{parent}_FK"/>
+ <ck value="{table}_CK"/>
+ <un value="{table}_{column}_UN"/>
+ <idx value="{table}_{column}_IDX"/>
+ <colck value="CK_{table}_{column}"/>
+ <column_foreign_key value="{ref table}_{ref column}"/>
+ <ui value="{entity} PK"/>
+ <relation_attribute value="{ref entity}_{ref attribute}"/>
+ </constraints>
+ <glossaries>
+ </glossaries>
+ </naming_standard_rules>
+<comparemapping>
+</comparemapping>
+ <engineering_params>
+ <delete_without_origin value="false"/>
+ <engineer_coordinates value="true"/>
+ <engineer_generated value="false"/>
+ <show_engineering_intree value="false"/>
+ <apply_naming_std value="false"/>
+ <use_pref_abbreviation value="true"/>
+ <upload_directory value=""/>
+ <date_format value="YYYY/MM/DD HH24:MI:SS"/>
+ <timestamp_format value="YYYY/MM/DD HH24:MI:SS.FF"/>
+ <timestamp_tz_format value="YYYY/MM/DD HH24:MI:SS.FFTZH:TZM"/>
+ </engineering_params>
+ <eng_compare show_sel_prop_only="true" not_apply_for_new_objects="true" exclude_from_tree="false">
+ <entity_table>
+ <property name="Name" selected="true"/>
+ <property name="Short Name / Abbreviation" selected="true"/>
+ <property name="Comment" selected="true"/>
+ <property name="Comment in RDBMS" selected="true"/>
+ <property name="Notes" selected="true"/>
+ <property name="Temporary Table Scope" selected="true"/>
+ <property name="Table Type" selected="true"/>
+ <property name="Structured Type" selected="true"/>
+ <property name="Type Substitution (Super-Type Object)" selected="true"/>
+ <property name="Min Volumes" selected="true"/>
+ <property name="Expected Volumes" selected="true"/>
+ <property name="Max Volumes" selected="true"/>
+ <property name="Growth Percent" selected="true"/>
+ <property name="Growth Type" selected="true"/>
+ <property name="Normal Form" selected="true"/>
+ <property name="Adequately Normalized" selected="true"/>
+ </entity_table>
+ <attribute_column>
+ <property name="Name" selected="true"/>
+ <property name="Data Type" selected="true"/>
+ <property name="Data Type Kind" selected="true"/>
+ <property name="Mandatory" selected="true"/>
+ <property name="Default Value" selected="true"/>
+ <property name="Check Constraint Name" selected="true"/>
+ <property name="Use Domain Constraint" selected="true"/>
+ <property name="Check Constraint" selected="true"/>
+ <property name="Range Constraint" selected="true"/>
+ <property name="LOV Constraint" selected="true"/>
+ <property name="Comment" selected="true"/>
+ <property name="Comment in RDBMS" selected="true"/>
+ <property name="Notes" selected="true"/>
+ <property name="Source Type" selected="true"/>
+ <property name="Formula Description" selected="true"/>
+ <property name="Type Substitution" selected="true"/>
+ <property name="Scope" selected="true"/>
+ </attribute_column>
+ <key_index>
+ <property name="Name" selected="true"/>
+ <property name="Comment" selected="true"/>
+ <property name="Comment in RDBMS" selected="true"/>
+ <property name="Notes" selected="true"/>
+ <property name="Primary Key" selected="true"/>
+ <property name="Attributes/Columns" selected="true"/>
+ </key_index>
+ <relation_fk>
+ <property name="Name" selected="true"/>
+ <property name="Delete Rule" selected="true"/>
+ <property name="Comment" selected="true"/>
+ <property name="Comment in RDBMS" selected="true"/>
+ <property name="Notes" selected="true"/>
+ </relation_fk>
+ <entityview_view>
+ <property name="Name" selected="true"/>
+ <property name="Comment" selected="true"/>
+ <property name="Comment in RDBMS" selected="true"/>
+ <property name="Notes" selected="true"/>
+ <property name="Structured Type" selected="true"/>
+ <property name="Where" selected="true"/>
+ <property name="Having" selected="true"/>
+ <property name="User Defined SQL" selected="true"/>
+ </entityview_view>
+ </eng_compare>
+ <naming_options>
+ <model_options objectid="B082B14A-BEA8-D8A7-D661-197F34766ED3">
+ <naming_option class_name="oracle.dbtools.crest.model.design.relational.Table" max_name_length="30" case_type="2" valid_characters="" all_valid="true" />
+ <naming_option class_name="oracle.dbtools.crest.model.design.relational.Column" max_name_length="30" case_type="2" valid_characters="" all_valid="true" />
+ <naming_option class_name="oracle.dbtools.crest.model.design.relational.TableView" max_name_length="30" case_type="2" valid_characters="" all_valid="true" />
+ <naming_option class_name="oracle.dbtools.crest.model.design.constraint.TableLevelConstraint" max_name_length="30" case_type="2" valid_characters="" all_valid="true" />
+ <naming_option class_name="oracle.dbtools.crest.model.design.relational.FKIndexAssociation" max_name_length="30" case_type="2" valid_characters="" all_valid="true" />
+ <naming_option class_name="oracle.dbtools.crest.model.design.relational.Index" max_name_length="30" case_type="2" valid_characters="" all_valid="true" />
+ </model_options>
+ <model_options objectid="E3665D68-35D3-8757-63ED-30AEFB972A2C">
+ <naming_option class_name="oracle.dbtools.crest.model.design.logical.Entity" max_name_length="254" case_type="2" valid_characters="[a-z][A-Z][0-9]" all_valid="false" />
+ <naming_option class_name="oracle.dbtools.crest.model.design.logical.Attribute" max_name_length="254" case_type="2" valid_characters="[a-z][A-Z][0-9] " all_valid="false" />
+ <naming_option class_name="oracle.dbtools.crest.model.design.logical.EntityView" max_name_length="254" case_type="2" valid_characters="[a-z][A-Z][0-9] " all_valid="false" />
+ </model_options>
+ </naming_options>
+ <merge_conflicts>
+ </merge_conflicts>
+ <deleted_files>
+ </deleted_files>
+</settings> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dr_custom_scripts.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dr_custom_scripts.xml
new file mode 100644
index 00000000..3580f0f0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dr_custom_scripts.xml
@@ -0,0 +1,360 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<dr_custom_scripts>
+ <scr id="D36CE536-D575-BE5C-625F-23DE23913C6B" name="Complex rule - check comments demo" object="Table" engine="Mozilla Rhino" type="Warning" var="table" library="my first library" method="checkcomments" purpose="validation" >
+ <script>
+ <![CDATA[var ruleMessage;
+var errType;
+var table;
+function checkcomments(object){
+ result = true;
+ ruleMessage="";
+ if(table.getCommentInRDBMS().equals("")){
+ ruleMessage="no comments in RDBMS defined";
+ errType="Problem:";
+ result = false;
+ }
+ if(table.getComment().equals("")){
+ if(ruleMessage.equals("")){
+ ruleMessage="no comments defined";
+ }else{
+ ruleMessage= ruleMessage +" , no comments defined";
+ }
+ errType="Error";
+ return false;
+ }
+ return result;
+}]]>
+ </script>
+ </scr>
+ <scr id="0BAA564F-AB5F-D776-2E4F-31FDB3047F69" name="Tables to lower case - Rhino" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[tables = model.getTableSet().toArray();
+for (var t = 0; t<tables.length;t++){
+ table = tables[t];
+ name = table.getName().toLowerCase();
+ table.setName(name);
+ columns = table.getElements();
+ size = table.getElementsCollection().size();
+ for (var i = 0; i < size; i++) {
+ column = columns[i];
+ cname = column.getName().toLowerCase();
+ column.setName(cname);
+ }
+ table.setDirty(true);
+ keys = table.getKeys();
+ for (var i = 0; i < keys.length; i++) {
+ key = keys[i];
+ if(!key.isFK()){
+ kname = key.getName().toLowerCase();
+ key.setName(kname);
+ }else{
+ kname = key.getFKAssociation().getName().toLowerCase();
+ key.getFKAssociation().setName(kname);
+ key.getFKAssociation().setDirty(true);
+ }
+ }
+}]]>
+ </script>
+ </scr>
+ <scr id="B673F271-4836-DD48-15AC-487DDECCAF49" name="Tables to upper case - JRuby" object="relational" engine="JSR 223 JRuby Engine" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[tables =$model.getTableSet().toArray()
+for t in 0..tables.length-1
+ table = tables[t]
+ name = table.getName().upcase
+ table.setName(name)
+ columns = table.getElements()
+ size = table.getElementsCollection().size()-1
+ for i in 0..size
+ column = columns[i]
+ cname = column.getName().upcase
+ column.setName(cname)
+ end
+ keys = table.getKeys()
+ for i in 0..keys.length-1
+ key = keys[i]
+ kname = key.getName().upcase
+ key.setName(kname)
+ end
+end]]>
+ </script>
+ </scr>
+ <scr id="3E7C4F9E-9FCB-56C7-086F-F976F9A66384" name="Tables to upper case - JRuby - library usage" object="relational" engine="JSR 223 JRuby Engine" type="" var="model" library="Jruby lib" method="tables_up" purpose="transformation" >
+ <script>
+ <![CDATA[def tables_up(model)
+tables = model.getTableSet().toArray()
+for t in 0..tables.length-1
+ table = tables[t]
+ name = table.getName().upcase
+ table.setName(name)
+ columns = table.getElements()
+ size = table.getElementsCollection().size()-1
+ for i in 0..size
+ column = columns[i]
+ cname = column.getName().upcase
+ column.setName(cname)
+ end
+ keys = table.getKeys()
+ for i in 0..keys.length-1
+ key = keys[i]
+ kname = key.getName().upcase
+ key.setName(kname)
+ end
+end
+return true
+end]]>
+ </script>
+ </scr>
+ <scr id="E60A5A28-BB9B-3787-10E7-259DF900B9E6" name="Table abbreviation to column" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[tables = model.getTableSet().toArray();
+for (var t = 0; t<tables.length;t++){
+ table = tables[t];
+ abbr = table.getAbbreviation()+"_";
+ if(!"_".equals(abbr)){
+ columns = table.getElements();
+ for (var i = 0; i < columns.length; i++) {
+ column = columns[i];
+ cname = column.getName();
+ if(!cname.startsWith(abbr)){
+ column.setName(abbr+cname);
+ }
+ }
+ }
+}]]>
+ </script>
+ </scr>
+ <scr id="9BE4E26C-36D8-A92C-ADEA-F183327DC239" name="Remove Table abbr from column" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[tables = model.getTableSet().toArray();
+for (var t = 0; t<tables.length;t++){
+ table = tables[t];
+ abbr = table.getAbbreviation()+"_";
+ count = table.getAbbreviation().length()+1;
+ if(!"_".equals(abbr)){
+ columns = table.getElements();
+ for (var i = 0; i < columns.length; i++) {
+ column = columns[i];
+ cname = column.getName();
+ if(cname.startsWith(abbr)){
+ column.setName(cname.substring(count));
+ table.setDirty(true);
+ }
+ }
+ }
+}]]>
+ </script>
+ </scr>
+ <scr id="23BE8827-D732-72B0-C6E6-266EFE116EDD" name="Table template" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[var t_name = "table_template";
+var p_name = "ctemplateID";
+template = model.getTableSet().getByName(t_name);
+if(template!=null){
+ tcolumns = template.getElements();
+ tables = model.getTableSet().toArray();
+ for (var t = 0; t<tables.length;t++){
+ table = tables[t];
+ // compare name ignoring the case
+ if(!table.getName().equalsIgnoreCase(t_name)){
+ for (var i = 0; i < tcolumns.length; i++) {
+ column = tcolumns[i];
+ col = table.getColumnByProperty(p_name,column.getObjectID());
+ if(col==null){
+ col = table.createColumn();
+ }
+ column.copy(col);
+ //set property after copy otherwise it'll be cleared
+ col.setProperty(p_name,column.getObjectID());
+ table.setDirty(true);
+ }
+ }
+ }
+}]]>
+ </script>
+ </scr>
+ <scr id="5A8A151A-13FD-4B0A-E233-E3C5126BA02C" name="Tables to upper case - Rhino" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[tables = model.getTableSet().toArray();
+for (var t = 0; t<tables.length;t++){
+ table = tables[t];
+ name = table.getName().toUpperCase();
+ table.setName(name);
+ columns = table.getElements();
+ size = table.getElementsCollection().size();
+ for (var i = 0; i < size; i++) {
+ column = columns[i];
+ cname = column.getName().toUpperCase();
+ column.setName(cname);
+ }
+ table.setDirty(true);
+ keys = table.getKeys();
+ for (var i = 0; i < keys.length; i++) {
+ key = keys[i];
+ if(!key.isFK()){
+ kname = key.getName().toUpperCase();
+ key.setName(kname);
+ }else{
+ kname = key.getFKAssociation().getName().toUpperCase();
+ key.getFKAssociation().setName(kname);
+ key.getFKAssociation().setDirty(true);
+ }
+ }
+}]]>
+ </script>
+ </scr>
+ <scr id="0528C35C-F29B-E7BB-57AC-37BA2780A98D" name="Table template - uses column name" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[// version without usage of dynamic properties, columns are found by column name
+// this allow reuse of already existing columns
+var t_name = "table_template";
+template = model.getTableSet().getByName(t_name);
+if(template!=null){
+ tcolumns = template.getElements();
+ tables = model.getTableSet().toArray();
+ for (var t = 0; t<tables.length;t++){
+ table = tables[t];
+ // compare name ignoring the case
+ if(!table.getName().equalsIgnoreCase(t_name)){
+ for (var i = 0; i < tcolumns.length; i++) {
+ column = tcolumns[i];
+ col = table.getElementByName(column.getName());
+ if(col==null){
+ col = table.createColumn();
+ }
+ column.copy(col);
+ table.setDirty(true);
+ }
+ }
+ }
+}]]>
+ </script>
+ </scr>
+ <scr id="6279C414-90DD-A52B-4CEB-8D49AB31DC10" name="Copy Comments to Comments in RDBMS" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[max_length = 4000;
+function copyComments(object){
+ if(object.getCommentInRDBMS().equals("")){
+ if(!object.getComment().equals("")){
+ if(object.getComment().length()>max_length){
+ object.setCommentInRDBMS(object.getComment().substring(0, max_length));
+ }else{
+ object.setCommentInRDBMS(object.getComment());
+ }
+ object.setDirty(true);
+ }
+ }
+}
+
+tables = model.getTableSet().toArray();
+for (var t = 0; t<tables.length;t++){
+ table = tables[t]
+ copyComments(table);
+ columns = table.getElements();
+ size = table.getElementsCollection().size();
+ for (var i = 0; i < columns.length; i++) {
+ column = columns[i];
+ copyComments(column);
+ }
+ keys = table.getKeys();
+ for (var i = 0; i < keys.length; i++) {
+ key = keys[i];
+ if(!key.isFK()){
+ copyComments(key);
+ }else{
+ copyComments(key.getFKAssociation());
+ }
+ }
+}]]>
+ </script>
+ </scr>
+ <scr id="7C4EDFC0-26EA-859C-DBD9-AC9345DEAF98" name="Create index on FK" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" >
+ <script>
+ <![CDATA[function getIndex(tab,cols){
+ keys = tab.getKeys();
+ for (var i = 0; i < keys.length; i++) {
+ index = keys[i];
+ if(!(index.isPK() || index.isUnique()) && !index.isFK() && index.isIndexForColumns(cols)){
+ return index
+ }
+ }
+ return null;
+}
+
+tables = model.getTableSet().toArray();
+for (var t = 0; t<tables.length;t++){
+ table = tables[t];
+ indexes = table.getKeys();
+ for (var i = 0; i < indexes.length; i++) {
+ index = indexes[i];
+ if(index.isFK()){
+ columns = index.getColumns();
+ if(columns.length>0){
+ newIndex = getIndex(table,columns);
+ if(newIndex==null){
+ newIndex = table.createIndex()
+ table.setDirty(true);
+ for (var k = 0; k < columns.length; k++){
+ newIndex.add(columns[k]);
+ }
+ }
+ }
+ }
+ }
+}]]>
+ </script>
+ </scr>
+
+ <lib id="B310E434-78AE-6AED-EA94-6808B0262483" name="my first library" engine="Mozilla Rhino" methods="checkcomments" >
+ <script>
+ <![CDATA[var ruleMessage;
+var errType;
+var table;
+function checkcomments(object){
+ result = true;
+ ruleMessage="";
+ if(table.getCommentInRDBMS().equals("")){
+ ruleMessage="no comments in RDBMS defined";
+ errType="Problem:";
+ result = false;
+ }
+ if(table.getComment().equals("")){
+ if(ruleMessage.equals("")){
+ ruleMessage="no comments defined";
+ }else{
+ ruleMessage= ruleMessage +" , no comments defined";
+ }
+ errType="Error";
+ return false;
+ }
+ return result;
+}]]>
+ </script>
+ </lib>
+ <lib id="2518F33A-DE50-9E1D-7216-DD2A0FD6B84C" name="Jruby lib" engine="JRuby Engine" methods="tables_up" >
+ <script>
+ <![CDATA[def tables_up(model)
+tables = model.getTableSet().toArray()
+for t in 0..tables.length-1
+ table = tables[t]
+ name = table.getName().upcase
+ table.setName(name)
+ columns = table.getElements()
+ size = table.getElementsCollection().size()-1
+ for i in 0..size
+ column = columns[i]
+ cname = column.getName().upcase
+ column.setName(cname)
+ end
+ keys = table.getKeys()
+ for i in 0..keys.length-1
+ key = keys[i]
+ kname = key.getName().upcase
+ key.setName(kname)
+ end
+end
+return true
+end]]>
+ </script>
+ </lib>
+</dr_custom_scripts> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/Logical.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/Logical.xml
new file mode 100644
index 00000000..0403a605
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/Logical.xml
@@ -0,0 +1,7 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<LogicalDesign class="oracle.dbtools.crest.model.design.logical.LogicalDesign" name="Logical" id="E3665D68-35D3-8757-63ED-30AEFB972A2C" mainViewID="AFCEF013-4CF2-4A5A-79A3-31521C1CA20A">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 21:58:45 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<shouldBeOpen>false</shouldBeOpen>
+</LogicalDesign> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/16464F5A-64BE-D2ED-91E0-BCBD0AA34680.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/16464F5A-64BE-D2ED-91E0-BCBD0AA34680.xml
new file mode 100644
index 00000000..6904bb54
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/16464F5A-64BE-D2ED-91E0-BCBD0AA34680.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="16464F5A-64BE-D2ED-91E0-BCBD0AA34680" directorySegmentName="seg_0" name="TestResults">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:11:26 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/1BEAB532-23CA-8628-0C97-7CAD39119A4E.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/1BEAB532-23CA-8628-0C97-7CAD39119A4E.xml
new file mode 100644
index 00000000..9f54c6cd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/1BEAB532-23CA-8628-0C97-7CAD39119A4E.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="1BEAB532-23CA-8628-0C97-7CAD39119A4E" directorySegmentName="seg_0" name="TestCaseArgs">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:38:18 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/24150FB1-B00F-4F69-6F77-49ECB58F0F66.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/24150FB1-B00F-4F69-6F77-49ECB58F0F66.xml
new file mode 100644
index 00000000..3a02553a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/24150FB1-B00F-4F69-6F77-49ECB58F0F66.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="24150FB1-B00F-4F69-6F77-49ECB58F0F66" directorySegmentName="seg_0" name="BuildSources">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:54:55 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/28DD93CF-D058-7343-CD47-E9B435E1AC16.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/28DD93CF-D058-7343-CD47-E9B435E1AC16.xml
new file mode 100644
index 00000000..3a9992c9
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/28DD93CF-D058-7343-CD47-E9B435E1AC16.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="28DD93CF-D058-7343-CD47-E9B435E1AC16" directorySegmentName="seg_0" name="TestResultFiles">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:12:51 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/2F6ACC6D-3D17-537D-8ADF-F8424395B345.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/2F6ACC6D-3D17-537D-8ADF-F8424395B345.xml
new file mode 100644
index 00000000..4ea40fc7
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/2F6ACC6D-3D17-537D-8ADF-F8424395B345.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="2F6ACC6D-3D17-537D-8ADF-F8424395B345" directorySegmentName="seg_0" name="GlobalRsrcStatuses">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:17:42 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39.xml
new file mode 100644
index 00000000..e3300354
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39" directorySegmentName="seg_0" name="FailureReasons">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-22 11:47:11 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4579B792-2F35-D72A-1A3B-C7E53C41A766.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4579B792-2F35-D72A-1A3B-C7E53C41A766.xml
new file mode 100644
index 00000000..e35d8bc0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4579B792-2F35-D72A-1A3B-C7E53C41A766.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="4579B792-2F35-D72A-1A3B-C7E53C41A766" directorySegmentName="seg_0" name="TestResultMsgs">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:13:03 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4D937E7C-3A28-E52D-89C0-EC8804C62367.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4D937E7C-3A28-E52D-89C0-EC8804C62367.xml
new file mode 100644
index 00000000..7b2d1d01
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4D937E7C-3A28-E52D-89C0-EC8804C62367.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="4D937E7C-3A28-E52D-89C0-EC8804C62367" directorySegmentName="seg_0" name="FailureCategories">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-22 11:47:19 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/504221DA-1B57-4EAD-39DB-40FD553E9FA2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/504221DA-1B57-4EAD-39DB-40FD553E9FA2.xml
new file mode 100644
index 00000000..e536867c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/504221DA-1B57-4EAD-39DB-40FD553E9FA2.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="504221DA-1B57-4EAD-39DB-40FD553E9FA2" directorySegmentName="seg_0" name="Builds">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:52:15 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/6A886CEE-579B-48FF-63F6-0FB03393FBF6.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/6A886CEE-579B-48FF-63F6-0FB03393FBF6.xml
new file mode 100644
index 00000000..20424c7c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/6A886CEE-579B-48FF-63F6-0FB03393FBF6.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="6A886CEE-579B-48FF-63F6-0FB03393FBF6" directorySegmentName="seg_0" name="SchedGroups">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:16:15 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/7AE36CC1-A030-63E5-6EF3-72FCD04815EE.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/7AE36CC1-A030-63E5-6EF3-72FCD04815EE.xml
new file mode 100644
index 00000000..9475385d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/7AE36CC1-A030-63E5-6EF3-72FCD04815EE.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="7AE36CC1-A030-63E5-6EF3-72FCD04815EE" directorySegmentName="seg_0" name="TestBoxes">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:34:30 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90367AFB-BA2D-A918-46B9-1E5DE53ACC48.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90367AFB-BA2D-A918-46B9-1E5DE53ACC48.xml
new file mode 100644
index 00000000..96b815e1
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90367AFB-BA2D-A918-46B9-1E5DE53ACC48.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="90367AFB-BA2D-A918-46B9-1E5DE53ACC48" directorySegmentName="seg_0" name="BuildBlacklist">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:59:31 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90F477EE-35D6-21A7-B693-E5724FB07476.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90F477EE-35D6-21A7-B693-E5724FB07476.xml
new file mode 100644
index 00000000..6bcf734c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90F477EE-35D6-21A7-B693-E5724FB07476.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="90F477EE-35D6-21A7-B693-E5724FB07476" directorySegmentName="seg_0" name="TestSets">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:11:20 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/9F78B73C-056D-DDEF-8C50-A9DA76B9E724.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/9F78B73C-056D-DDEF-8C50-A9DA76B9E724.xml
new file mode 100644
index 00000000..d672b27e
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/9F78B73C-056D-DDEF-8C50-A9DA76B9E724.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="9F78B73C-056D-DDEF-8C50-A9DA76B9E724" directorySegmentName="seg_0" name="BuildTypes">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:52:32 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A352A20F-310D-E285-FBC9-90DD0DA7BB9B.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A352A20F-310D-E285-FBC9-90DD0DA7BB9B.xml
new file mode 100644
index 00000000..301a3f28
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A352A20F-310D-E285-FBC9-90DD0DA7BB9B.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="A352A20F-310D-E285-FBC9-90DD0DA7BB9B" directorySegmentName="seg_0" name="TestBoxStatuses">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:09:55 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A6A5F317-479C-A0DD-CAAE-9DCB56B29D40.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A6A5F317-479C-A0DD-CAAE-9DCB56B29D40.xml
new file mode 100644
index 00000000..a6f31387
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A6A5F317-479C-A0DD-CAAE-9DCB56B29D40.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="A6A5F317-479C-A0DD-CAAE-9DCB56B29D40" directorySegmentName="seg_0" name="RequirementSets">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:14:04 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B36A186B-CDB3-7851-8C38-12EA8D50EAEB.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B36A186B-CDB3-7851-8C38-12EA8D50EAEB.xml
new file mode 100644
index 00000000..7e22bcc2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B36A186B-CDB3-7851-8C38-12EA8D50EAEB.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="B36A186B-CDB3-7851-8C38-12EA8D50EAEB" directorySegmentName="seg_0" name="RequirementsNum">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:14:37 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B82DAF9A-6F99-5CF6-4D99-A391BAD66192.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B82DAF9A-6F99-5CF6-4D99-A391BAD66192.xml
new file mode 100644
index 00000000..aa84dcf3
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B82DAF9A-6F99-5CF6-4D99-A391BAD66192.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="B82DAF9A-6F99-5CF6-4D99-A391BAD66192" directorySegmentName="seg_0" name="TestCases">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:34:30 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C332E3D7-638B-6CA8-24BF-383CA8659A3A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C332E3D7-638B-6CA8-24BF-383CA8659A3A.xml
new file mode 100644
index 00000000..f093d805
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C332E3D7-638B-6CA8-24BF-383CA8659A3A.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="C332E3D7-638B-6CA8-24BF-383CA8659A3A" directorySegmentName="seg_0" name="SchedQueues">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:09:44 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C79482B8-771B-FAD8-0337-163E3A45003A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C79482B8-771B-FAD8-0337-163E3A45003A.xml
new file mode 100644
index 00000000..3550b18c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C79482B8-771B-FAD8-0337-163E3A45003A.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="C79482B8-771B-FAD8-0337-163E3A45003A" directorySegmentName="seg_0" name="GlobalResources">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:13:16 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/D09E0DE5-99D6-2991-032A-A8A124F6ACBA.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/D09E0DE5-99D6-2991-032A-A8A124F6ACBA.xml
new file mode 100644
index 00000000..1e10ffb7
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/D09E0DE5-99D6-2991-032A-A8A124F6ACBA.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="D09E0DE5-99D6-2991-032A-A8A124F6ACBA" directorySegmentName="seg_0" name="TestResultValues">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:11:32 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DCC79294-5434-1DED-298C-6473DEE59FBA.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DCC79294-5434-1DED-298C-6473DEE59FBA.xml
new file mode 100644
index 00000000..7891dab7
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DCC79294-5434-1DED-298C-6473DEE59FBA.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="DCC79294-5434-1DED-298C-6473DEE59FBA" directorySegmentName="seg_0" name="TestResultFailures">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-22 11:46:51 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DE366053-6F7A-7F42-ABA3-00E583098C37.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DE366053-6F7A-7F42-ABA3-00E583098C37.xml
new file mode 100644
index 00000000..145b2c76
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DE366053-6F7A-7F42-ABA3-00E583098C37.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="DE366053-6F7A-7F42-ABA3-00E583098C37" directorySegmentName="seg_0" name="TestGroups">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:34:30 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/E93BBF08-067B-A665-39F3-CF488A6547B2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/E93BBF08-067B-A665-39F3-CF488A6547B2.xml
new file mode 100644
index 00000000..c8632bf7
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/E93BBF08-067B-A665-39F3-CF488A6547B2.xml
@@ -0,0 +1,52 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="E93BBF08-067B-A665-39F3-CF488A6547B2" directorySegmentName="seg_0" name="RequirementsText">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:14:21 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<adequatelyNormalized>NO</adequatelyNormalized>
+<expectedVolumes>0</expectedVolumes>
+<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName>
+<growthPercent>0</growthPercent>
+<growthType>Year</growthType>
+<maxVolumes>9999999</maxVolumes>
+<minVolumes>0</minVolumes>
+<normalForm>Third</normalForm>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<fontStyle>1</fontStyle>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Attribute</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Datatype</foType>
+<colorRGB>-16744448</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>PK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>FK Element</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>UK Element</foType>
+<colorRGB>-16776961</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Not Null</foType>
+<colorRGB>-65536</colorRGB>
+</FontObject>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Key</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Entity> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/876CB767-80BA-6C8E-AACA-F1CCC95C445E.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/876CB767-80BA-6C8E-AACA-F1CCC95C445E.xml
new file mode 100644
index 00000000..31ddc417
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/876CB767-80BA-6C8E-AACA-F1CCC95C445E.xml
@@ -0,0 +1,16 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Note class="oracle.dbtools.crest.model.design.Note" name="Note_1" id="876CB767-80BA-6C8E-AACA-F1CCC95C445E" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:43:49 UTC</createdTime>
+<comment>Priority, scheduling time, and testgroup dependencies are associated with SchedGroup membership.</comment>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Note> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/D487AFDC-4027-F824-EA29-5C6D0ABB9E1E.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/D487AFDC-4027-F824-EA29-5C6D0ABB9E1E.xml
new file mode 100644
index 00000000..9152a7c6
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/D487AFDC-4027-F824-EA29-5C6D0ABB9E1E.xml
@@ -0,0 +1,16 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Note class="oracle.dbtools.crest.model.design.Note" name="Note_3" id="D487AFDC-4027-F824-EA29-5C6D0ABB9E1E" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:57:21 UTC</createdTime>
+<comment>Testsuite and build sources.</comment>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<fonts>
+<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr">
+<foType>Title</foType>
+<colorRGB>-16777216</colorRGB>
+</FontObject>
+</fonts>
+</Note> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/01537211-CCFB-0A1E-B43B-E8C641B69471.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/01537211-CCFB-0A1E-B43B-E8C641B69471.xml
new file mode 100644
index 00000000..e8b317cd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/01537211-CCFB-0A1E-B43B-E8C641B69471.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="WhichTestcaseArgs" id="01537211-CCFB-0A1E-B43B-E8C641B69471" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:57:18 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>*</sourceCardinality>
+<sourceEntity>90F477EE-35D6-21A7-B693-E5724FB07476</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>1BEAB532-23CA-8628-0C97-7CAD39119A4E</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/02096BBB-0795-1759-1E26-2877BE36BB59.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/02096BBB-0795-1759-1E26-2877BE36BB59.xml
new file mode 100644
index 00000000..48df0a07
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/02096BBB-0795-1759-1E26-2877BE36BB59.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="NestedTestResults" id="02096BBB-0795-1759-1E26-2877BE36BB59" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:16:26 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/0CCF1DE3-7916-9054-BEA6-C601FF564DB2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/0CCF1DE3-7916-9054-BEA6-C601FF564DB2.xml
new file mode 100644
index 00000000..e5304e1d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/0CCF1DE3-7916-9054-BEA6-C601FF564DB2.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestBoxGrouping" id="0CCF1DE3-7916-9054-BEA6-C601FF564DB2" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:35:28 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>true</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>false</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>7AE36CC1-A030-63E5-6EF3-72FCD04815EE</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>6A886CEE-579B-48FF-63F6-0FB03393FBF6</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/10867E70-94CE-FDAF-6B6E-2742D3A49E57.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/10867E70-94CE-FDAF-6B6E-2742D3A49E57.xml
new file mode 100644
index 00000000..ed642271
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/10867E70-94CE-FDAF-6B6E-2742D3A49E57.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="ReasonForBlacklisting" id="10867E70-94CE-FDAF-6B6E-2742D3A49E57" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-22 11:56:22 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>90367AFB-BA2D-A918-46B9-1E5DE53ACC48</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/11710A55-6423-1904-841A-C7D2AB8CEEBF.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/11710A55-6423-1904-841A-C7D2AB8CEEBF.xml
new file mode 100644
index 00000000..4c37ff79
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/11710A55-6423-1904-841A-C7D2AB8CEEBF.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestResultValues\" id="11710A55-6423-1904-841A-C7D2AB8CEEBF" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:17:15 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>D09E0DE5-99D6-2991-032A-A8A124F6ACBA</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/1C189437-742B-B999-C955-7754C8ADB089.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/1C189437-742B-B999-C955-7754C8ADB089.xml
new file mode 100644
index 00000000..ee340833
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/1C189437-742B-B999-C955-7754C8ADB089.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="SchedTestGroupMembership" id="1C189437-742B-B999-C955-7754C8ADB089" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:46:08 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>*</sourceCardinality>
+<sourceEntity>6A886CEE-579B-48FF-63F6-0FB03393FBF6</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>DE366053-6F7A-7F42-ABA3-00E583098C37</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/34733942-1305-4CA1-47EB-ACE724B04E69.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/34733942-1305-4CA1-47EB-ACE724B04E69.xml
new file mode 100644
index 00000000..bde14e2c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/34733942-1305-4CA1-47EB-ACE724B04E69.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestResultFiles" id="34733942-1305-4CA1-47EB-ACE724B04E69" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:16:58 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>28DD93CF-D058-7343-CD47-E9B435E1AC16</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3563C940-E524-7F96-7AE0-DAC3C1C17AFC.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3563C940-E524-7F96-7AE0-DAC3C1C17AFC.xml
new file mode 100644
index 00000000..0d924eae
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3563C940-E524-7F96-7AE0-DAC3C1C17AFC.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestedBuild" id="3563C940-E524-7F96-7AE0-DAC3C1C17AFC" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 10:14:03 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>true</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>false</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>504221DA-1B57-4EAD-39DB-40FD553E9FA2</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>90F477EE-35D6-21A7-B693-E5724FB07476</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C.xml
new file mode 100644
index 00000000..f0a22501
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="BuildSource" id="3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:55:43 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>false</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>24150FB1-B00F-4F69-6F77-49ECB58F0F66</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>6A886CEE-579B-48FF-63F6-0FB03393FBF6</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3B7C8913-EB6A-47B1-27D0-E2C85EE9048B.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3B7C8913-EB6A-47B1-27D0-E2C85EE9048B.xml
new file mode 100644
index 00000000..9a95a66a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3B7C8913-EB6A-47B1-27D0-E2C85EE9048B.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="NumericalRequirement" id="3B7C8913-EB6A-47B1-27D0-E2C85EE9048B" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:41:40 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>true</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>false</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>A6A5F317-479C-A0DD-CAAE-9DCB56B29D40</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>B36A186B-CDB3-7851-8C38-12EA8D50EAEB</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/518CE489-97B4-C05C-07A2-E3DBF14EE267.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/518CE489-97B4-C05C-07A2-E3DBF14EE267.xml
new file mode 100644
index 00000000..7987194b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/518CE489-97B4-C05C-07A2-E3DBF14EE267.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestResultFailureReason" id="518CE489-97B4-C05C-07A2-E3DBF14EE267" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-22 11:58:35 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>DCC79294-5434-1DED-298C-6473DEE59FBA</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/68A0C3E1-0FA1-8414-A361-33B08A8EDB39.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/68A0C3E1-0FA1-8414-A361-33B08A8EDB39.xml
new file mode 100644
index 00000000..bf2200dc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/68A0C3E1-0FA1-8414-A361-33B08A8EDB39.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="FailureRegardingTestResult" id="68A0C3E1-0FA1-8414-A361-33B08A8EDB39" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-22 11:48:45 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>DCC79294-5434-1DED-298C-6473DEE59FBA</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7497D76B-781B-3BDD-D797-FFBDB974F772.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7497D76B-781B-3BDD-D797-FFBDB974F772.xml
new file mode 100644
index 00000000..43673229
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7497D76B-781B-3BDD-D797-FFBDB974F772.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="GlobalResourceDependencies" id="7497D76B-781B-3BDD-D797-FFBDB974F772" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:42:25 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>*</sourceCardinality>
+<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>C79482B8-771B-FAD8-0337-163E3A45003A</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635.xml
new file mode 100644
index 00000000..dd75d4cb
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestResultMessages" id="7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:17:23 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>4579B792-2F35-D72A-1A3B-C7E53C41A766</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/89A83E25-364B-6B73-0613-FEAD875EF9FB.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/89A83E25-364B-6B73-0613-FEAD875EF9FB.xml
new file mode 100644
index 00000000..e8a4730c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/89A83E25-364B-6B73-0613-FEAD875EF9FB.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestcaseArguments" id="89A83E25-364B-6B73-0613-FEAD875EF9FB" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:40:39 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>false</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>1BEAB532-23CA-8628-0C97-7CAD39119A4E</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2.xml
new file mode 100644
index 00000000..9d086559
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="WhatToRun" id="8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:41:56 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>*</sourceCardinality>
+<sourceEntity>C332E3D7-638B-6CA8-24BF-383CA8659A3A</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>1BEAB532-23CA-8628-0C97-7CAD39119A4E</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51.xml
new file mode 100644
index 00000000..b50ed32a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="WhichResource" id="9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:52:20 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>2F6ACC6D-3D17-537D-8ADF-F8424395B345</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>C79482B8-771B-FAD8-0337-163E3A45003A</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/A182A65A-47AE-5D00-9A30-BC20AB050BF2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/A182A65A-47AE-5D00-9A30-BC20AB050BF2.xml
new file mode 100644
index 00000000..b29652bd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/A182A65A-47AE-5D00-9A30-BC20AB050BF2.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestSetResult" id="A182A65A-47AE-5D00-9A30-BC20AB050BF2" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:15:48 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>90F477EE-35D6-21A7-B693-E5724FB07476</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B346381F-48FE-E495-01A7-E22EC26AEE8A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B346381F-48FE-E495-01A7-E22EC26AEE8A.xml
new file mode 100644
index 00000000..ba60f398
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B346381F-48FE-E495-01A7-E22EC26AEE8A.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestGroupMember" id="B346381F-48FE-E495-01A7-E22EC26AEE8A" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:37:24 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>*</sourceCardinality>
+<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>DE366053-6F7A-7F42-ABA3-00E583098C37</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B3596116-540F-6397-ECE4-58A386644E15.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B3596116-540F-6397-ECE4-58A386644E15.xml
new file mode 100644
index 00000000..d4f9edd8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B3596116-540F-6397-ECE4-58A386644E15.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestcaseDependencies" id="B3596116-540F-6397-ECE4-58A386644E15" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:39:51 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>*</sourceCardinality>
+<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/BAD8EC05-6F14-4E38-366C-B4B660C6F38A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/BAD8EC05-6F14-4E38-366C-B4B660C6F38A.xml
new file mode 100644
index 00000000..da1e2a8f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/BAD8EC05-6F14-4E38-366C-B4B660C6F38A.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="InFailureCategory" id="BAD8EC05-6F14-4E38-366C-B4B660C6F38A" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-22 11:57:18 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>true</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>false</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>4D937E7C-3A28-E52D-89C0-EC8804C62367</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE.xml
new file mode 100644
index 00000000..d75c9a0a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="WhichTestBox" id="C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:59:42 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>*</sourceCardinality>
+<sourceEntity>90F477EE-35D6-21A7-B693-E5724FB07476</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>7AE36CC1-A030-63E5-6EF3-72FCD04815EE</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/CCD38E11-8557-EB34-2651-07EB29E83FA6.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/CCD38E11-8557-EB34-2651-07EB29E83FA6.xml
new file mode 100644
index 00000000..bf216b5d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/CCD38E11-8557-EB34-2651-07EB29E83FA6.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestSuiteSource" id="CCD38E11-8557-EB34-2651-07EB29E83FA6" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:56:11 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>24150FB1-B00F-4F69-6F77-49ECB58F0F66</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>6A886CEE-579B-48FF-63F6-0FB03393FBF6</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E2A47942-ED55-E81D-4C71-9A134C49C147.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E2A47942-ED55-E81D-4C71-9A134C49C147.xml
new file mode 100644
index 00000000..5164076c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E2A47942-ED55-E81D-4C71-9A134C49C147.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestBox" id="E2A47942-ED55-E81D-4C71-9A134C49C147" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:43:14 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>false</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>7AE36CC1-A030-63E5-6EF3-72FCD04815EE</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>A352A20F-310D-E285-FBC9-90DD0DA7BB9B</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E4FE88E9-EE21-B43B-B0FE-A153E38246F9.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E4FE88E9-EE21-B43B-B0FE-A153E38246F9.xml
new file mode 100644
index 00000000..fc0ec020
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E4FE88E9-EE21-B43B-B0FE-A153E38246F9.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestcaseRequirements" id="E4FE88E9-EE21-B43B-B0FE-A153E38246F9" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:38:38 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>*</sourceCardinality>
+<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>A6A5F317-479C-A0DD-CAAE-9DCB56B29D40</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E62AE7DF-49EE-9280-B328-A867CBD273AE.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E62AE7DF-49EE-9280-B328-A867CBD273AE.xml
new file mode 100644
index 00000000..3121966f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E62AE7DF-49EE-9280-B328-A867CBD273AE.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="CurrentTestSet" id="E62AE7DF-49EE-9280-B328-A867CBD273AE" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:48:53 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>A352A20F-310D-E285-FBC9-90DD0DA7BB9B</sourceEntity>
+<targetCardinalityString>1</targetCardinalityString>
+<targetEntity>90F477EE-35D6-21A7-B693-E5724FB07476</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E74406B5-20F1-4323-DC99-6E45982CB606.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E74406B5-20F1-4323-DC99-6E45982CB606.xml
new file mode 100644
index 00000000..498ce1fb
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E74406B5-20F1-4323-DC99-6E45982CB606.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TextRequirements" id="E74406B5-20F1-4323-DC99-6E45982CB606" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:41:57 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>true</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>false</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>A6A5F317-479C-A0DD-CAAE-9DCB56B29D40</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>E93BBF08-067B-A665-39F3-CF488A6547B2</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EC4EB506-3DBE-7F36-6451-F31920EDAB52.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EC4EB506-3DBE-7F36-6451-F31920EDAB52.xml
new file mode 100644
index 00000000..18840e25
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EC4EB506-3DBE-7F36-6451-F31920EDAB52.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="AllocatedBy" id="EC4EB506-3DBE-7F36-6451-F31920EDAB52" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:44:47 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>true</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>7AE36CC1-A030-63E5-6EF3-72FCD04815EE</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>2F6ACC6D-3D17-537D-8ADF-F8424395B345</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880.xml
new file mode 100644
index 00000000..6fcc7e2b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880.xml
@@ -0,0 +1,17 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="BuildToType" id="EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880" directorySegmentName="seg_0">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:53:25 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<engineerTo>
+<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/>
+</engineerTo>
+<identifying>false</identifying>
+<optionalSource>true</optionalSource>
+<optionalTarget>false</optionalTarget>
+<sourceCardinality>1</sourceCardinality>
+<sourceEntity>9F78B73C-056D-DDEF-8C50-A9DA76B9E724</sourceEntity>
+<targetCardinalityString>*</targetCardinalityString>
+<targetEntity>504221DA-1B57-4EAD-39DB-40FD553E9FA2</targetEntity>
+<transferable>true</transferable>
+</Relation> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF.xml
new file mode 100644
index 00000000..e947c03a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF.xml
@@ -0,0 +1,40 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Failure Tracking" id="016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-22 12:01:22 UTC</createdTime>
+<autoRoute>false</autoRoute>
+<boxInbox>true</boxInbox>
+<showLegend>false</showLegend>
+<showLabels>false</showLabels>
+<showGrid>false</showGrid>
+<diagramColor>-1</diagramColor>
+<display>false</display>
+<notation>0</notation>
+<objectViews>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39" otype="Entity" vid="D1B4D1DF-E3AB-F84A-F479-87FB68F0A2D2">
+<bounds x="1270" y="448" width="151" height="41"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="4D937E7C-3A28-E52D-89C0-EC8804C62367" otype="Entity" vid="37DED3CC-443D-FC8B-A30D-07BF0D742C62">
+<bounds x="1270" y="522" width="152" height="43"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="DCC79294-5434-1DED-298C-6473DEE59FBA" otype="Entity" vid="95A5D57E-9986-0942-BCE8-4B9F5F46AE30">
+<bounds x="1087" y="460" width="157" height="51"/>
+</OView>
+</objectViews>
+<connectors>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="518CE489-97B4-C05C-07A2-E3DBF14EE267" otype="Relation" vid_source="95A5D57E-9986-0942-BCE8-4B9F5F46AE30" vid_target="D1B4D1DF-E3AB-F84A-F479-87FB68F0A2D2">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1244" y="474"/>
+<point x="1270" y="474"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="BAD8EC05-6F14-4E38-366C-B4B660C6F38A" otype="Relation" vid_source="D1B4D1DF-E3AB-F84A-F479-87FB68F0A2D2" vid_target="37DED3CC-443D-FC8B-A30D-07BF0D742C62">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1345" y="489"/>
+<point x="1345" y="522"/>
+</points>
+</Connector>
+</connectors>
+</Diagram> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/32D718B4-250F-95DC-37F0-C0A817F69020.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/32D718B4-250F-95DC-37F0-C0A817F69020.xml
new file mode 100644
index 00000000..6493425b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/32D718B4-250F-95DC-37F0-C0A817F69020.xml
@@ -0,0 +1,70 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Outputs" id="32D718B4-250F-95DC-37F0-C0A817F69020">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:19:53 UTC</createdTime>
+<autoRoute>false</autoRoute>
+<boxInbox>true</boxInbox>
+<showLegend>false</showLegend>
+<showLabels>false</showLabels>
+<showGrid>false</showGrid>
+<diagramColor>-1</diagramColor>
+<display>false</display>
+<notation>0</notation>
+<objectViews>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="16464F5A-64BE-D2ED-91E0-BCBD0AA34680" otype="Entity" vid="636E76B2-6F21-38E5-BF29-D4C078AC8F61">
+<bounds x="1014" y="625" width="121" height="102"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="28DD93CF-D058-7343-CD47-E9B435E1AC16" otype="Entity" vid="89BDF7A8-D79D-A869-BE57-BD2E1C2B290C">
+<bounds x="1190" y="610" width="131" height="41"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="4579B792-2F35-D72A-1A3B-C7E53C41A766" otype="Entity" vid="D72D72DA-F9C0-CE9C-E6A6-7A44DA7656DC">
+<bounds x="1190" y="710" width="131" height="41"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="90F477EE-35D6-21A7-B693-E5724FB07476" otype="Entity" vid="0A09F0EB-AF09-D080-F1B5-EC4E3693C1C5">
+<bounds x="824" y="652" width="141" height="51"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="D09E0DE5-99D6-2991-032A-A8A124F6ACBA" otype="Entity" vid="239CADB1-5F1D-1286-1C79-0DCD91157E84">
+<bounds x="1190" y="662" width="131" height="39"/>
+</OView>
+</objectViews>
+<connectors>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="02096BBB-0795-1759-1E26-2877BE36BB59" otype="Relation" vid_source="636E76B2-6F21-38E5-BF29-D4C078AC8F61" vid_target="636E76B2-6F21-38E5-BF29-D4C078AC8F61">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1135" y="676"/>
+<point x="1150" y="676"/>
+<point x="1150" y="742"/>
+<point x="1074" y="742"/>
+<point x="1074" y="727"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="11710A55-6423-1904-841A-C7D2AB8CEEBF" otype="Relation" vid_source="636E76B2-6F21-38E5-BF29-D4C078AC8F61" vid_target="239CADB1-5F1D-1286-1C79-0DCD91157E84">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1135" y="691"/>
+<point x="1190" y="691"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="34733942-1305-4CA1-47EB-ACE724B04E69" otype="Relation" vid_source="636E76B2-6F21-38E5-BF29-D4C078AC8F61" vid_target="89BDF7A8-D79D-A869-BE57-BD2E1C2B290C">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1135" y="638"/>
+<point x="1190" y="638"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635" otype="Relation" vid_source="636E76B2-6F21-38E5-BF29-D4C078AC8F61" vid_target="D72D72DA-F9C0-CE9C-E6A6-7A44DA7656DC">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1135" y="718"/>
+<point x="1190" y="718"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="A182A65A-47AE-5D00-9A30-BC20AB050BF2" otype="Relation" vid_source="0A09F0EB-AF09-D080-F1B5-EC4E3693C1C5" vid_target="636E76B2-6F21-38E5-BF29-D4C078AC8F61">
+<lineWidth>1</lineWidth>
+<points>
+<point x="965" y="677"/>
+<point x="1014" y="677"/>
+</points>
+</Connector>
+</connectors>
+</Diagram> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/571DBBAF-CDDA-1C46-4220-D1319C0EEC00.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/571DBBAF-CDDA-1C46-4220-D1319C0EEC00.xml
new file mode 100644
index 00000000..25df5afc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/571DBBAF-CDDA-1C46-4220-D1319C0EEC00.xml
@@ -0,0 +1,24 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Persistent Test Manager Data" id="571DBBAF-CDDA-1C46-4220-D1319C0EEC00">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:19:18 UTC</createdTime>
+<autoRoute>false</autoRoute>
+<boxInbox>true</boxInbox>
+<showLegend>false</showLegend>
+<showLabels>false</showLabels>
+<showGrid>false</showGrid>
+<diagramColor>-1</diagramColor>
+<display>false</display>
+<notation>0</notation>
+<objectViews>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="2F6ACC6D-3D17-537D-8ADF-F8424395B345" otype="Entity" vid="B4E5F358-5BC8-9B06-4A13-EDF705ED9089">
+<bounds x="110" y="570" width="151" height="61"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="A352A20F-310D-E285-FBC9-90DD0DA7BB9B" otype="Entity" vid="8747577F-8999-3CBF-1376-1DD291702774">
+<bounds x="300" y="570" width="151" height="61"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="C332E3D7-638B-6CA8-24BF-383CA8659A3A" otype="Entity" vid="F053C992-CB30-88B3-66FF-F4E522C60155">
+<bounds x="499" y="570" width="136" height="61"/>
+</OView>
+</objectViews>
+</Diagram> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83.xml
new file mode 100644
index 00000000..c248a58e
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83.xml
@@ -0,0 +1,127 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Configuration" id="65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 08:58:45 UTC</createdTime>
+<autoRoute>false</autoRoute>
+<boxInbox>true</boxInbox>
+<showLegend>false</showLegend>
+<showLabels>false</showLabels>
+<showGrid>false</showGrid>
+<diagramColor>-1</diagramColor>
+<display>false</display>
+<notation>0</notation>
+<objectViews>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="1BEAB532-23CA-8628-0C97-7CAD39119A4E" otype="Entity" vid="459DD9CF-0825-0BAE-7BBA-FADAA3B895BB">
+<bounds x="680" y="419" width="161" height="52"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="24150FB1-B00F-4F69-6F77-49ECB58F0F66" otype="Entity" vid="398E8687-F10E-D31E-DD4E-EA0A6A7868A3">
+<bounds x="273" y="96" width="138" height="61"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="6A886CEE-579B-48FF-63F6-0FB03393FBF6" otype="Entity" vid="E301FF23-DE18-19FB-9A6A-9F170D26B939">
+<bounds x="180" y="250" width="131" height="71"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="7AE36CC1-A030-63E5-6EF3-72FCD04815EE" otype="Entity" vid="B06DA0BE-1DA3-3AB7-06CD-E7EA9FDC0B3E">
+<bounds x="101" y="95" width="131" height="61"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="A6A5F317-479C-A0DD-CAAE-9DCB56B29D40" otype="Entity" vid="49F6288A-70A0-788D-3FEE-BE0053D8D44C">
+<bounds x="680" y="130" width="161" height="41"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="B36A186B-CDB3-7851-8C38-12EA8D50EAEB" otype="Entity" vid="9E4B525D-2B00-0B76-39EE-0C0F74693333">
+<bounds x="600" y="30" width="141" height="31"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="B82DAF9A-6F99-5CF6-4D99-A391BAD66192" otype="Entity" vid="2C49F347-32B8-CA7C-2646-4F16FDDA087E">
+<bounds x="680" y="250" width="161" height="71"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="C79482B8-771B-FAD8-0337-163E3A45003A" otype="Entity" vid="8FAC087B-6133-162A-207B-3FAFB7B41E98">
+<bounds x="908" y="250" width="153" height="31"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="DE366053-6F7A-7F42-ABA3-00E583098C37" otype="Entity" vid="61150DED-91F4-1AE3-BD02-4EDC4CC0D98F">
+<bounds x="430" y="250" width="131" height="71"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="E93BBF08-067B-A665-39F3-CF488A6547B2" otype="Entity" vid="C41DA40C-A50A-BDCC-4DA0-2DCA7874C1A2">
+<bounds x="789" y="30" width="132" height="31"/>
+</OView>
+</objectViews>
+<connectors>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="0CCF1DE3-7916-9054-BEA6-C601FF564DB2" otype="Relation" vid_source="B06DA0BE-1DA3-3AB7-06CD-E7EA9FDC0B3E" vid_target="E301FF23-DE18-19FB-9A6A-9F170D26B939">
+<lineWidth>1</lineWidth>
+<points>
+<point x="206" y="156"/>
+<point x="206" y="250"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="1C189437-742B-B999-C955-7754C8ADB089" otype="Relation" vid_source="E301FF23-DE18-19FB-9A6A-9F170D26B939" vid_target="61150DED-91F4-1AE3-BD02-4EDC4CC0D98F">
+<lineWidth>1</lineWidth>
+<points>
+<point x="311" y="285"/>
+<point x="430" y="285"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C" otype="Relation" vid_source="398E8687-F10E-D31E-DD4E-EA0A6A7868A3" vid_target="E301FF23-DE18-19FB-9A6A-9F170D26B939">
+<lineWidth>1</lineWidth>
+<points>
+<point x="292" y="157"/>
+<point x="292" y="250"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3B7C8913-EB6A-47B1-27D0-E2C85EE9048B" otype="Relation" vid_source="49F6288A-70A0-788D-3FEE-BE0053D8D44C" vid_target="9E4B525D-2B00-0B76-39EE-0C0F74693333">
+<lineWidth>1</lineWidth>
+<points>
+<point x="710" y="130"/>
+<point x="710" y="61"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="7497D76B-781B-3BDD-D797-FFBDB974F772" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="8FAC087B-6133-162A-207B-3FAFB7B41E98">
+<lineWidth>1</lineWidth>
+<points>
+<point x="841" y="265"/>
+<point x="908" y="265"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="89A83E25-364B-6B73-0613-FEAD875EF9FB" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="459DD9CF-0825-0BAE-7BBA-FADAA3B895BB">
+<lineWidth>1</lineWidth>
+<points>
+<point x="760" y="321"/>
+<point x="760" y="419"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="B346381F-48FE-E495-01A7-E22EC26AEE8A" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="61150DED-91F4-1AE3-BD02-4EDC4CC0D98F">
+<lineWidth>1</lineWidth>
+<points>
+<point x="680" y="285"/>
+<point x="561" y="285"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="B3596116-540F-6397-ECE4-58A386644E15" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="2C49F347-32B8-CA7C-2646-4F16FDDA087E">
+<lineWidth>1</lineWidth>
+<points>
+<point x="841" y="285"/>
+<point x="856" y="285"/>
+<point x="856" y="336"/>
+<point x="760" y="336"/>
+<point x="760" y="321"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="CCD38E11-8557-EB34-2651-07EB29E83FA6" otype="Relation" vid_source="398E8687-F10E-D31E-DD4E-EA0A6A7868A3" vid_target="E301FF23-DE18-19FB-9A6A-9F170D26B939">
+<lineWidth>1</lineWidth>
+<points>
+<point x="302" y="157"/>
+<point x="302" y="250"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E4FE88E9-EE21-B43B-B0FE-A153E38246F9" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="49F6288A-70A0-788D-3FEE-BE0053D8D44C">
+<lineWidth>1</lineWidth>
+<points>
+<point x="760" y="250"/>
+<point x="760" y="171"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E74406B5-20F1-4323-DC99-6E45982CB606" otype="Relation" vid_source="49F6288A-70A0-788D-3FEE-BE0053D8D44C" vid_target="C41DA40C-A50A-BDCC-4DA0-2DCA7874C1A2">
+<lineWidth>1</lineWidth>
+<points>
+<point x="815" y="130"/>
+<point x="815" y="61"/>
+</points>
+</Connector>
+</connectors>
+</Diagram> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/AFCEF013-4CF2-4A5A-79A3-31521C1CA20A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/AFCEF013-4CF2-4A5A-79A3-31521C1CA20A.xml
new file mode 100644
index 00000000..14a7566f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/AFCEF013-4CF2-4A5A-79A3-31521C1CA20A.xml
@@ -0,0 +1,306 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogical" name="Logical" id="AFCEF013-4CF2-4A5A-79A3-31521C1CA20A">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:02:17 UTC</createdTime>
+<autoRoute>false</autoRoute>
+<boxInbox>true</boxInbox>
+<showLegend>false</showLegend>
+<showLabels>true</showLabels>
+<showGrid>true</showGrid>
+<diagramColor>-1</diagramColor>
+<legendPosX>265</legendPosX>
+<legendPosY>490</legendPosY>
+<display>false</display>
+<notation>0</notation>
+<objectViews>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="16464F5A-64BE-D2ED-91E0-BCBD0AA34680" otype="Entity" vid="5B100733-B921-D478-15B5-3BE9A7747A87">
+<bounds x="1014" y="625" width="121" height="102"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="1BEAB532-23CA-8628-0C97-7CAD39119A4E" otype="Entity" vid="62F579AD-F97F-1F92-7C5F-525AE1A2F26C">
+<bounds x="680" y="419" width="161" height="52"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="24150FB1-B00F-4F69-6F77-49ECB58F0F66" otype="Entity" vid="B3D29C8C-8482-D7AF-BE58-122AB07FB853">
+<bounds x="273" y="96" width="138" height="61"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="28DD93CF-D058-7343-CD47-E9B435E1AC16" otype="Entity" vid="ABB72A58-23E7-DF85-4B01-74F467F60284">
+<bounds x="1190" y="610" width="131" height="41"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="2F6ACC6D-3D17-537D-8ADF-F8424395B345" otype="Entity" vid="40AB3AA2-7D9F-7BA7-AB96-050F27CF81AB">
+<bounds x="110" y="570" width="151" height="51"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39" otype="Entity" vid="BE78445F-B005-8F1A-E390-120DCC587063">
+<bounds x="1270" y="448" width="151" height="41"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="4579B792-2F35-D72A-1A3B-C7E53C41A766" otype="Entity" vid="BA629852-B837-F348-59DD-12899B260C79">
+<bounds x="1190" y="710" width="131" height="41"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="4D937E7C-3A28-E52D-89C0-EC8804C62367" otype="Entity" vid="109E2A3F-B942-1D32-CB1C-4F60260ACF5C">
+<bounds x="1270" y="522" width="152" height="43"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="504221DA-1B57-4EAD-39DB-40FD553E9FA2" otype="Entity" vid="F4CED71A-65B7-151C-3ADC-26F25043F168">
+<bounds x="1092" y="301" width="151" height="70"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="6A886CEE-579B-48FF-63F6-0FB03393FBF6" otype="Entity" vid="81A8E233-0690-CBFE-6102-F71A991903FC">
+<bounds x="180" y="250" width="131" height="71"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="7AE36CC1-A030-63E5-6EF3-72FCD04815EE" otype="Entity" vid="C8DAF849-7026-3615-7FC8-4397BFC6CA14">
+<bounds x="101" y="95" width="131" height="61"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.TVNote" oid="876CB767-80BA-6C8E-AACA-F1CCC95C445E" otype="Note" vid="593FF096-DB74-2562-91B0-A4F1423FEBA7">
+<bounds x="292" y="336" width="149" height="61"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="90367AFB-BA2D-A918-46B9-1E5DE53ACC48" otype="Entity" vid="5A1E3970-E7C2-5B4A-B4FC-A4224370E349">
+<bounds x="1270" y="300" width="145" height="72"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="90F477EE-35D6-21A7-B693-E5724FB07476" otype="Entity" vid="B6946DC3-6424-2A37-D668-5BD36839859C">
+<bounds x="824" y="652" width="141" height="51"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="9F78B73C-056D-DDEF-8C50-A9DA76B9E724" otype="Entity" vid="EEE8DCBD-05DB-E390-AE27-14DFF3B0DD56">
+<bounds x="1091" y="205" width="151" height="63"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="A352A20F-310D-E285-FBC9-90DD0DA7BB9B" otype="Entity" vid="27BF1041-8402-6396-1A77-2223122117A1">
+<bounds x="292" y="570" width="148" height="51"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="A6A5F317-479C-A0DD-CAAE-9DCB56B29D40" otype="Entity" vid="AB9AED98-F420-DDD6-02BA-ABA20D05AFB3">
+<bounds x="680" y="130" width="161" height="41"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="B36A186B-CDB3-7851-8C38-12EA8D50EAEB" otype="Entity" vid="8B654282-58D6-084A-69E2-3C8D7E390802">
+<bounds x="600" y="30" width="141" height="31"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="B82DAF9A-6F99-5CF6-4D99-A391BAD66192" otype="Entity" vid="2F2EDF15-4992-FE58-E928-D09AF0373D9E">
+<bounds x="680" y="250" width="161" height="71"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="C332E3D7-638B-6CA8-24BF-383CA8659A3A" otype="Entity" vid="03B42717-C78B-007E-11B3-EEA11AABA415">
+<bounds x="472" y="570" width="136" height="51"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="C79482B8-771B-FAD8-0337-163E3A45003A" otype="Entity" vid="8D1A1E0A-0651-0364-F81D-EC5D599DF29A">
+<bounds x="909" y="251" width="132" height="51"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="D09E0DE5-99D6-2991-032A-A8A124F6ACBA" otype="Entity" vid="2446BDB4-EEEF-A6B8-6F46-4C1208EDECC2">
+<bounds x="1190" y="662" width="131" height="39"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.TVNote" oid="D487AFDC-4027-F824-EA29-5C6D0ABB9E1E" otype="Note" vid="583B257A-5AD8-026F-84FF-AB3956387595">
+<bounds x="322" y="179" width="89" height="40"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="DCC79294-5434-1DED-298C-6473DEE59FBA" otype="Entity" vid="8689850E-1426-9DCF-EF62-4753AFEE7BE6">
+<bounds x="1087" y="460" width="157" height="51"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="DE366053-6F7A-7F42-ABA3-00E583098C37" otype="Entity" vid="CAF127DE-45F6-6BCE-8FAB-7BAE679347E1">
+<bounds x="430" y="250" width="131" height="71"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="E93BBF08-067B-A665-39F3-CF488A6547B2" otype="Entity" vid="2862D2B6-5340-9024-1DF2-E4408EA96B6E">
+<bounds x="789" y="30" width="132" height="31"/>
+</OView>
+</objectViews>
+<connectors>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="01537211-CCFB-0A1E-B43B-E8C641B69471" otype="Relation" vid_source="B6946DC3-6424-2A37-D668-5BD36839859C" vid_target="62F579AD-F97F-1F92-7C5F-525AE1A2F26C">
+<lineWidth>1</lineWidth>
+<points>
+<point x="832" y="652"/>
+<point x="832" y="471"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="02096BBB-0795-1759-1E26-2877BE36BB59" otype="Relation" vid_source="5B100733-B921-D478-15B5-3BE9A7747A87" vid_target="5B100733-B921-D478-15B5-3BE9A7747A87">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1135" y="676"/>
+<point x="1150" y="676"/>
+<point x="1150" y="742"/>
+<point x="1074" y="742"/>
+<point x="1074" y="727"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="0CCF1DE3-7916-9054-BEA6-C601FF564DB2" otype="Relation" vid_source="C8DAF849-7026-3615-7FC8-4397BFC6CA14" vid_target="81A8E233-0690-CBFE-6102-F71A991903FC">
+<lineWidth>1</lineWidth>
+<points>
+<point x="206" y="156"/>
+<point x="206" y="250"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="10867E70-94CE-FDAF-6B6E-2742D3A49E57" otype="Relation" vid_source="5A1E3970-E7C2-5B4A-B4FC-A4224370E349" vid_target="BE78445F-B005-8F1A-E390-120DCC587063">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1342" y="372"/>
+<point x="1342" y="448"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="11710A55-6423-1904-841A-C7D2AB8CEEBF" otype="Relation" vid_source="5B100733-B921-D478-15B5-3BE9A7747A87" vid_target="2446BDB4-EEEF-A6B8-6F46-4C1208EDECC2">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1135" y="690"/>
+<point x="1190" y="690"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="1C189437-742B-B999-C955-7754C8ADB089" otype="Relation" vid_source="81A8E233-0690-CBFE-6102-F71A991903FC" vid_target="CAF127DE-45F6-6BCE-8FAB-7BAE679347E1">
+<lineWidth>1</lineWidth>
+<points>
+<point x="311" y="285"/>
+<point x="430" y="285"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="34733942-1305-4CA1-47EB-ACE724B04E69" otype="Relation" vid_source="5B100733-B921-D478-15B5-3BE9A7747A87" vid_target="ABB72A58-23E7-DF85-4B01-74F467F60284">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1135" y="638"/>
+<point x="1190" y="638"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3563C940-E524-7F96-7AE0-DAC3C1C17AFC" otype="Relation" vid_source="F4CED71A-65B7-151C-3ADC-26F25043F168" vid_target="B6946DC3-6424-2A37-D668-5BD36839859C">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1167" y="371"/>
+<point x="894" y="652"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C" otype="Relation" vid_source="B3D29C8C-8482-D7AF-BE58-122AB07FB853" vid_target="81A8E233-0690-CBFE-6102-F71A991903FC">
+<lineWidth>1</lineWidth>
+<points>
+<point x="300" y="157"/>
+<point x="300" y="250"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3B7C8913-EB6A-47B1-27D0-E2C85EE9048B" otype="Relation" vid_source="AB9AED98-F420-DDD6-02BA-ABA20D05AFB3" vid_target="8B654282-58D6-084A-69E2-3C8D7E390802">
+<lineWidth>1</lineWidth>
+<points>
+<point x="710" y="130"/>
+<point x="710" y="61"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="518CE489-97B4-C05C-07A2-E3DBF14EE267" otype="Relation" vid_source="8689850E-1426-9DCF-EF62-4753AFEE7BE6" vid_target="BE78445F-B005-8F1A-E390-120DCC587063">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1244" y="474"/>
+<point x="1270" y="474"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="68A0C3E1-0FA1-8414-A361-33B08A8EDB39" otype="Relation" vid_source="8689850E-1426-9DCF-EF62-4753AFEE7BE6" vid_target="5B100733-B921-D478-15B5-3BE9A7747A87">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1111" y="511"/>
+<point x="1111" y="625"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="7497D76B-781B-3BDD-D797-FFBDB974F772" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="8D1A1E0A-0651-0364-F81D-EC5D599DF29A">
+<lineWidth>1</lineWidth>
+<points>
+<point x="841" y="266"/>
+<point x="909" y="266"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635" otype="Relation" vid_source="5B100733-B921-D478-15B5-3BE9A7747A87" vid_target="BA629852-B837-F348-59DD-12899B260C79">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1135" y="718"/>
+<point x="1190" y="718"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="89A83E25-364B-6B73-0613-FEAD875EF9FB" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="62F579AD-F97F-1F92-7C5F-525AE1A2F26C">
+<lineWidth>1</lineWidth>
+<points>
+<point x="750" y="321"/>
+<point x="750" y="419"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2" otype="Relation" vid_source="03B42717-C78B-007E-11B3-EEA11AABA415" vid_target="62F579AD-F97F-1F92-7C5F-525AE1A2F26C">
+<lineWidth>1</lineWidth>
+<points>
+<point x="540" y="570"/>
+<point x="760" y="471"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51" otype="Relation" vid_source="40AB3AA2-7D9F-7BA7-AB96-050F27CF81AB" vid_target="8D1A1E0A-0651-0364-F81D-EC5D599DF29A">
+<lineWidth>1</lineWidth>
+<points>
+<point x="185" y="570"/>
+<point x="985" y="302"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="A182A65A-47AE-5D00-9A30-BC20AB050BF2" otype="Relation" vid_source="B6946DC3-6424-2A37-D668-5BD36839859C" vid_target="5B100733-B921-D478-15B5-3BE9A7747A87">
+<lineWidth>1</lineWidth>
+<points>
+<point x="965" y="677"/>
+<point x="1014" y="677"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="B346381F-48FE-E495-01A7-E22EC26AEE8A" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="CAF127DE-45F6-6BCE-8FAB-7BAE679347E1">
+<lineWidth>1</lineWidth>
+<points>
+<point x="680" y="285"/>
+<point x="561" y="285"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="B3596116-540F-6397-ECE4-58A386644E15" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="2F2EDF15-4992-FE58-E928-D09AF0373D9E">
+<lineWidth>1</lineWidth>
+<points>
+<point x="841" y="285"/>
+<point x="856" y="285"/>
+<point x="856" y="336"/>
+<point x="760" y="336"/>
+<point x="760" y="321"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="BAD8EC05-6F14-4E38-366C-B4B660C6F38A" otype="Relation" vid_source="BE78445F-B005-8F1A-E390-120DCC587063" vid_target="109E2A3F-B942-1D32-CB1C-4F60260ACF5C">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1345" y="489"/>
+<point x="1345" y="522"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE" otype="Relation" vid_source="B6946DC3-6424-2A37-D668-5BD36839859C" vid_target="C8DAF849-7026-3615-7FC8-4397BFC6CA14">
+<lineWidth>1</lineWidth>
+<points>
+<point x="894" y="652"/>
+<point x="166" y="156"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="CCD38E11-8557-EB34-2651-07EB29E83FA6" otype="Relation" vid_source="B3D29C8C-8482-D7AF-BE58-122AB07FB853" vid_target="81A8E233-0690-CBFE-6102-F71A991903FC">
+<lineWidth>1</lineWidth>
+<points>
+<point x="280" y="157"/>
+<point x="280" y="250"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E2A47942-ED55-E81D-4C71-9A134C49C147" otype="Relation" vid_source="C8DAF849-7026-3615-7FC8-4397BFC6CA14" vid_target="27BF1041-8402-6396-1A77-2223122117A1">
+<lineWidth>1</lineWidth>
+<points>
+<point x="166" y="156"/>
+<point x="330" y="570"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E4FE88E9-EE21-B43B-B0FE-A153E38246F9" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="AB9AED98-F420-DDD6-02BA-ABA20D05AFB3">
+<lineWidth>1</lineWidth>
+<points>
+<point x="760" y="250"/>
+<point x="760" y="171"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E62AE7DF-49EE-9280-B328-A867CBD273AE" otype="Relation" vid_source="27BF1041-8402-6396-1A77-2223122117A1" vid_target="B6946DC3-6424-2A37-D668-5BD36839859C">
+<lineWidth>1</lineWidth>
+<points>
+<point x="360" y="621"/>
+<point x="824" y="677"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E74406B5-20F1-4323-DC99-6E45982CB606" otype="Relation" vid_source="AB9AED98-F420-DDD6-02BA-ABA20D05AFB3" vid_target="2862D2B6-5340-9024-1DF2-E4408EA96B6E">
+<lineWidth>1</lineWidth>
+<points>
+<point x="815" y="130"/>
+<point x="815" y="61"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="EC4EB506-3DBE-7F36-6451-F31920EDAB52" otype="Relation" vid_source="C8DAF849-7026-3615-7FC8-4397BFC6CA14" vid_target="40AB3AA2-7D9F-7BA7-AB96-050F27CF81AB">
+<lineWidth>1</lineWidth>
+<points>
+<point x="130" y="156"/>
+<point x="130" y="570"/>
+</points>
+</Connector>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880" otype="Relation" vid_source="EEE8DCBD-05DB-E390-AE27-14DFF3B0DD56" vid_target="F4CED71A-65B7-151C-3ADC-26F25043F168">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1166" y="243"/>
+<point x="1167" y="301"/>
+</points>
+</Connector>
+</connectors>
+</Diagram> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/F936BE6D-7A74-1B57-7564-41C1E13B973B.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/F936BE6D-7A74-1B57-7564-41C1E13B973B.xml
new file mode 100644
index 00000000..bcc0009f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/F936BE6D-7A74-1B57-7564-41C1E13B973B.xml
@@ -0,0 +1,33 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Inputs" id="F936BE6D-7A74-1B57-7564-41C1E13B973B">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-21 09:08:50 UTC</createdTime>
+<autoRoute>false</autoRoute>
+<boxInbox>true</boxInbox>
+<showLegend>false</showLegend>
+<showLabels>false</showLabels>
+<showGrid>false</showGrid>
+<diagramColor>-1</diagramColor>
+<display>false</display>
+<notation>0</notation>
+<objectViews>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="504221DA-1B57-4EAD-39DB-40FD553E9FA2" otype="Entity" vid="EA3885E3-FEE4-031B-1751-1C6351610836">
+<bounds x="1091" y="476" width="151" height="70"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="90367AFB-BA2D-A918-46B9-1E5DE53ACC48" otype="Entity" vid="86784B28-925D-6EAF-24D8-27DE22A0A93B">
+<bounds x="1090" y="376" width="151" height="68"/>
+</OView>
+<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="9F78B73C-056D-DDEF-8C50-A9DA76B9E724" otype="Entity" vid="1B62E962-0DFC-D5AE-0AC4-33E14F65E825">
+<bounds x="1297" y="477" width="151" height="71"/>
+</OView>
+</objectViews>
+<connectors>
+<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880" otype="Relation" vid_source="1B62E962-0DFC-D5AE-0AC4-33E14F65E825" vid_target="EA3885E3-FEE4-031B-1751-1C6351610836">
+<lineWidth>1</lineWidth>
+<points>
+<point x="1297" y="511"/>
+<point x="1242" y="511"/>
+</points>
+</Connector>
+</connectors>
+</Diagram> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap.xml
new file mode 100644
index 00000000..6811f63f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap.xml
@@ -0,0 +1,3 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<ExtendedMap class="oracle.dbtools.crest.model.xtdmapping.ExtendedMap">
+</ExtendedMap> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap_RMB082B14A-BEA8-D8A7-D661-197F34766ED3.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap_RMB082B14A-BEA8-D8A7-D661-197F34766ED3.xml
new file mode 100644
index 00000000..7ea5df08
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap_RMB082B14A-BEA8-D8A7-D661-197F34766ED3.xml
@@ -0,0 +1,3 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<RMExtendedMap class="oracle.dbtools.crest.model.xtdmapping.RMExtendedMap">
+</RMExtendedMap> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rdbms/TestManagerDatabase_RDBMSSites.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rdbms/TestManagerDatabase_RDBMSSites.xml
new file mode 100644
index 00000000..e0c5dad0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rdbms/TestManagerDatabase_RDBMSSites.xml
@@ -0,0 +1,2 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<metadatadoc version="2.0"/> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3.xml
new file mode 100644
index 00000000..76bdad85
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3.xml
@@ -0,0 +1,8 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<relationalModel class="oracle.dbtools.crest.model.design.relational.RelationalDesign" name="Relational_1" id="B082B14A-BEA8-D8A7-D661-197F34766ED3" mainViewID="6CEC5843-B4DD-D9B0-54D4-2845569D5E9F">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 21:58:45 UTC</createdTime>
+<ownerDesignName>TestManagerDatabase</ownerDesignName>
+<shouldBeOpen>false</shouldBeOpen>
+<selectedRDBMSSite>32076570-2523-435C-2E92-BF29817DFF70</selectedRDBMSSite>
+</relationalModel> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3/subviews/6CEC5843-B4DD-D9B0-54D4-2845569D5E9F.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3/subviews/6CEC5843-B4DD-D9B0-54D4-2845569D5E9F.xml
new file mode 100644
index 00000000..44b040be
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3/subviews/6CEC5843-B4DD-D9B0-54D4-2845569D5E9F.xml
@@ -0,0 +1,13 @@
+<?xml version = '1.0' encoding = 'UTF-8'?>
+<Diagram class="oracle.dbtools.crest.swingui.relational.DPVRelational" name="Relational_1" id="6CEC5843-B4DD-D9B0-54D4-2845569D5E9F">
+<createdBy>bird</createdBy>
+<createdTime>2012-08-20 22:02:17 UTC</createdTime>
+<autoRoute>false</autoRoute>
+<boxInbox>true</boxInbox>
+<showLegend>false</showLegend>
+<showLabels>false</showLabels>
+<showGrid>false</showGrid>
+<diagramColor>-1</diagramColor>
+<display>false</display>
+<notation>0</notation>
+</Diagram> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/types.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/types.xml
new file mode 100644
index 00000000..64fa7ab8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/types.xml
@@ -0,0 +1,933 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<logtypes>
+ <logicaltype name="Audio" objectid="LOGDT005">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">BINARY, size</mapping>
+ <mapping rdbms="SQL Server 2000">BINARY, size</mapping>
+ <mapping rdbms="DB2/390 8">BLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="BFile" objectid="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10034">
+ <mapping rdbms="Oracle Database 11g">BFILE</mapping>
+ <mapping rdbms="Oracle Database 10g">BFILE</mapping>
+ <mapping rdbms="Oracle9i">BFILE</mapping>
+ <mapping rdbms="SQL Server 2005">VARCHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">DATALINK</mapping>
+ <mapping rdbms="DB2/UDB 7.1">DATALINK</mapping>
+ </logicaltype>
+ <logicaltype name="BIGINT" objectid="LOGDT027">
+ <mapping rdbms="Oracle Database 11g">INTEGER</mapping>
+ <mapping rdbms="Oracle Database 10g">INTEGER</mapping>
+ <mapping rdbms="Oracle9i">INTEGER</mapping>
+ <mapping rdbms="SQL Server 2005">BIGINT</mapping>
+ <mapping rdbms="SQL Server 2000">BIGINT</mapping>
+ <mapping rdbms="DB2/390 8">INTEGER</mapping>
+ <mapping rdbms="DB2/390 7">INTEGER</mapping>
+ <mapping rdbms="DB2/UDB 8.1">INTEGER</mapping>
+ <mapping rdbms="DB2/UDB 7.1">INTEGER</mapping>
+ </logicaltype>
+ <logicaltype name="BINARY" objectid="LOGDT033">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">BINARY, size</mapping>
+ <mapping rdbms="SQL Server 2000">BINARY, size</mapping>
+ <mapping rdbms="DB2/390 8">BLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="BINARY DOUBLE" objectid="LOGDT056">
+ <mapping rdbms="Oracle Database 11g">BINARY_DOUBLE</mapping>
+ <mapping rdbms="Oracle Database 10g">BINARY_DOUBLE</mapping>
+ <mapping rdbms="Oracle9i">NUMBER</mapping>
+ <mapping rdbms="SQL Server 2005">FLOAT</mapping>
+ <mapping rdbms="SQL Server 2000">FLOAT</mapping>
+ <mapping rdbms="DB2/390 8">DOUBLE</mapping>
+ <mapping rdbms="DB2/390 7">DOUBLE</mapping>
+ <mapping rdbms="DB2/UDB 8.1">DOUBLE</mapping>
+ <mapping rdbms="DB2/UDB 7.1">DOUBLE</mapping>
+ </logicaltype>
+ <logicaltype name="BINARY FLOAT" objectid="LOGDT055">
+ <mapping rdbms="Oracle Database 11g">BINARY_FLOAT</mapping>
+ <mapping rdbms="Oracle Database 10g">BINARY_FLOAT</mapping>
+ <mapping rdbms="Oracle9i">NUMBER</mapping>
+ <mapping rdbms="SQL Server 2005">REAL</mapping>
+ <mapping rdbms="SQL Server 2000">REAL</mapping>
+ <mapping rdbms="DB2/390 8">REAL</mapping>
+ <mapping rdbms="DB2/390 7">REAL</mapping>
+ <mapping rdbms="DB2/UDB 8.1">REAL</mapping>
+ <mapping rdbms="DB2/UDB 7.1">REAL</mapping>
+ </logicaltype>
+ <logicaltype name="BIT" objectid="LOGDT034">
+ <mapping rdbms="Oracle Database 11g">CHAR</mapping>
+ <mapping rdbms="Oracle Database 10g">CHAR</mapping>
+ <mapping rdbms="Oracle9i">CHAR</mapping>
+ <mapping rdbms="SQL Server 2005">BIT</mapping>
+ <mapping rdbms="SQL Server 2000">BIT</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="BLOB" objectid="LOGDT029">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">IMAGE</mapping>
+ <mapping rdbms="SQL Server 2000">IMAGE</mapping>
+ <mapping rdbms="DB2/390 8">BLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="Boolean" objectid="LOGDT006">
+ <mapping rdbms="Oracle Database 11g">CHAR</mapping>
+ <mapping rdbms="Oracle Database 10g">CHAR</mapping>
+ <mapping rdbms="Oracle9i">CHAR</mapping>
+ <mapping rdbms="SQL Server 2005">BIT</mapping>
+ <mapping rdbms="SQL Server 2000">BIT</mapping>
+ <mapping rdbms="DB2/390 8">CHAR</mapping>
+ <mapping rdbms="DB2/390 7">CHAR</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR</mapping>
+ </logicaltype>
+ <logicaltype name="CHAR" objectid="LOGDT025">
+ <mapping rdbms="Oracle Database 11g">CHAR, size</mapping>
+ <mapping rdbms="Oracle Database 10g">CHAR, size</mapping>
+ <mapping rdbms="Oracle9i">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2005">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="CLOB" objectid="LOGDT028">
+ <mapping rdbms="Oracle Database 11g">CLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">CLOB</mapping>
+ <mapping rdbms="Oracle9i">CLOB</mapping>
+ <mapping rdbms="SQL Server 2005" size_default_value="max">VARCHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">TEXT</mapping>
+ <mapping rdbms="DB2/390 8">CLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="DATALINK" objectid="LOGDT030">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">BINARY</mapping>
+ <mapping rdbms="SQL Server 2000">BINARY</mapping>
+ <mapping rdbms="DB2/390 8">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">DATALINK</mapping>
+ <mapping rdbms="DB2/UDB 7.1">DATALINK</mapping>
+ </logicaltype>
+ <logicaltype name="DBURIType" objectid="LOGDT054">
+ <mapping rdbms="Oracle Database 11g">DBURITYPE</mapping>
+ <mapping rdbms="Oracle Database 10g">DBURITYPE</mapping>
+ <mapping rdbms="Oracle9i">DBURITYPE</mapping>
+ <mapping rdbms="SQL Server 2005">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">DATALINK</mapping>
+ <mapping rdbms="DB2/UDB 7.1">DATALINK</mapping>
+ </logicaltype>
+ <logicaltype name="DECIMAL" objectid="LOGDT026">
+ <mapping rdbms="Oracle Database 11g">NUMBER, precision, scale</mapping>
+ <mapping rdbms="Oracle Database 10g">NUMBER, precision, scale</mapping>
+ <mapping rdbms="Oracle9i">NUMBER, precision, scale</mapping>
+ <mapping rdbms="SQL Server 2005">DECIMAL, precision, scale</mapping>
+ <mapping rdbms="SQL Server 2000">DECIMAL, precision, scale</mapping>
+ <mapping rdbms="DB2/390 8">DECIMAL, precision, scale</mapping>
+ <mapping rdbms="DB2/390 7">DECIMAL, precision, scale</mapping>
+ <mapping rdbms="DB2/UDB 8.1">DECIMAL, precision, scale</mapping>
+ <mapping rdbms="DB2/UDB 7.1">DECIMAL, precision, scale</mapping>
+ </logicaltype>
+ <logicaltype name="DOUBLE" objectid="LOGDT020">
+ <mapping rdbms="Oracle Database 11g">NUMBER</mapping>
+ <mapping rdbms="Oracle Database 10g">NUMBER</mapping>
+ <mapping rdbms="Oracle9i">NUMBER</mapping>
+ <mapping rdbms="SQL Server 2005">BIGINT</mapping>
+ <mapping rdbms="SQL Server 2000">BIGINT</mapping>
+ <mapping rdbms="DB2/390 8">DOUBLE</mapping>
+ <mapping rdbms="DB2/390 7">DOUBLE</mapping>
+ <mapping rdbms="DB2/UDB 8.1">DOUBLE</mapping>
+ <mapping rdbms="DB2/UDB 7.1">DOUBLE</mapping>
+ </logicaltype>
+ <logicaltype name="Date" objectid="LOGDT007">
+ <mapping rdbms="Oracle Database 11g">DATE</mapping>
+ <mapping rdbms="Oracle Database 10g">DATE</mapping>
+ <mapping rdbms="Oracle9i">DATE</mapping>
+ <mapping rdbms="SQL Server 2005">DATETIME</mapping>
+ <mapping rdbms="SQL Server 2000">DATETIME</mapping>
+ <mapping rdbms="DB2/390 8">DATE</mapping>
+ <mapping rdbms="DB2/390 7">DATE</mapping>
+ <mapping rdbms="DB2/UDB 8.1">DATE</mapping>
+ <mapping rdbms="DB2/UDB 7.1">DATE</mapping>
+ </logicaltype>
+ <logicaltype name="Datetime" objectid="LOGDT008">
+ <mapping rdbms="Oracle Database 11g">DATE</mapping>
+ <mapping rdbms="Oracle Database 10g">DATE</mapping>
+ <mapping rdbms="Oracle9i">DATE</mapping>
+ <mapping rdbms="SQL Server 2005">DATETIME</mapping>
+ <mapping rdbms="SQL Server 2000">DATETIME</mapping>
+ <mapping rdbms="DB2/390 8">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/390 7">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping>
+ </logicaltype>
+ <logicaltype name="FLOAT" objectid="LOGDT021">
+ <mapping rdbms="Oracle Database 11g">FLOAT, precision</mapping>
+ <mapping rdbms="Oracle Database 10g">FLOAT, precision</mapping>
+ <mapping rdbms="Oracle9i">FLOAT, precision</mapping>
+ <mapping rdbms="SQL Server 2005">FLOAT, precision</mapping>
+ <mapping rdbms="SQL Server 2000">FLOAT, precision</mapping>
+ <mapping rdbms="DB2/390 8">FLOAT, precision</mapping>
+ <mapping rdbms="DB2/390 7">FLOAT, precision</mapping>
+ <mapping rdbms="DB2/UDB 8.1">FLOAT, precision</mapping>
+ <mapping rdbms="DB2/UDB 7.1">FLOAT, precision</mapping>
+ </logicaltype>
+ <logicaltype name="GRAPHIC" objectid="LOGDT031">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">BINARY</mapping>
+ <mapping rdbms="SQL Server 2000">BINARY</mapping>
+ <mapping rdbms="DB2/390 8">GRAPHIC, size</mapping>
+ <mapping rdbms="DB2/390 7">GRAPHIC, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">GRAPHIC, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">GRAPHIC, size</mapping>
+ </logicaltype>
+ <logicaltype name="HTTPURIType" objectid="LOGDT052">
+ <mapping rdbms="Oracle Database 11g">HTTPURITYPE</mapping>
+ <mapping rdbms="Oracle Database 10g">HTTPURITYPE</mapping>
+ <mapping rdbms="Oracle9i">HTTPURITYPE</mapping>
+ <mapping rdbms="SQL Server 2005">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="INTERVAL DAY TO SECOND" objectid="LOGDT049">
+ <mapping rdbms="Oracle Database 11g">INTERVAL DAY TO SECOND, precision, scale</mapping>
+ <mapping rdbms="Oracle Database 10g">INTERVAL DAY TO SECOND, precision, scale</mapping>
+ <mapping rdbms="Oracle9i">INTERVAL DAY TO SECOND, precision, scale</mapping>
+ <mapping rdbms="SQL Server 2005">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="INTERVAL YEAR TO MONTH" objectid="LOGDT048">
+ <mapping rdbms="Oracle Database 11g">INTERVAL YEAR TO MONTH, precision</mapping>
+ <mapping rdbms="Oracle Database 10g">INTERVAL YEAR TO MONTH, precision</mapping>
+ <mapping rdbms="Oracle9i">INTERVAL YEAR TO MONTH, precision</mapping>
+ <mapping rdbms="SQL Server 2005">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="Image" objectid="LOGDT010">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">IMAGE</mapping>
+ <mapping rdbms="SQL Server 2000">IMAGE</mapping>
+ <mapping rdbms="DB2/390 8">BLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="Integer" objectid="LOGDT011">
+ <mapping rdbms="Oracle Database 11g">INTEGER</mapping>
+ <mapping rdbms="Oracle Database 10g">INTEGER</mapping>
+ <mapping rdbms="Oracle9i">INTEGER</mapping>
+ <mapping rdbms="SQL Server 2005">INTEGER</mapping>
+ <mapping rdbms="SQL Server 2000">INTEGER</mapping>
+ <mapping rdbms="DB2/390 8">INTEGER</mapping>
+ <mapping rdbms="DB2/390 7">INTEGER</mapping>
+ <mapping rdbms="DB2/UDB 8.1">INTEGER</mapping>
+ <mapping rdbms="DB2/UDB 7.1">INTEGER</mapping>
+ </logicaltype>
+ <logicaltype name="Long Char" objectid="LogDes-1768A872-F385-FDBA-D95E-0CB63F5908E2@LOGDT10045">
+ <mapping rdbms="Oracle Database 11g">LONG</mapping>
+ <mapping rdbms="Oracle Database 10g">LONG</mapping>
+ <mapping rdbms="Oracle9i">LONG</mapping>
+ <mapping rdbms="SQL Server 2005">VARCHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="Long_Raw" objectid="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036">
+ <mapping rdbms="Oracle Database 11g">LONG RAW</mapping>
+ <mapping rdbms="Oracle Database 10g">LONG RAW</mapping>
+ <mapping rdbms="Oracle9i">LONG RAW</mapping>
+ <mapping rdbms="SQL Server 2005">VARBINARY, size</mapping>
+ <mapping rdbms="SQL Server 2000">VARBINARY, size</mapping>
+ <mapping rdbms="DB2/390 8">BLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="MONEY" objectid="LOGDT043">
+ <mapping rdbms="Oracle Database 11g">NUMBER, precision, scale</mapping>
+ <mapping rdbms="Oracle Database 10g">NUMBER, precision, scale</mapping>
+ <mapping rdbms="Oracle9i">NUMBER, precision, scale</mapping>
+ <mapping rdbms="SQL Server 2005">MONEY</mapping>
+ <mapping rdbms="SQL Server 2000">MONEY</mapping>
+ <mapping rdbms="DB2/390 8">DOUBLE</mapping>
+ <mapping rdbms="DB2/390 7">DOUBLE</mapping>
+ <mapping rdbms="DB2/UDB 8.1">DOUBLE</mapping>
+ <mapping rdbms="DB2/UDB 7.1">DOUBLE</mapping>
+ </logicaltype>
+ <logicaltype name="NCHAR" objectid="LOGDT035">
+ <mapping rdbms="Oracle Database 11g">NCHAR, size</mapping>
+ <mapping rdbms="Oracle Database 10g">NCHAR, size</mapping>
+ <mapping rdbms="Oracle9i">NCHAR, size</mapping>
+ <mapping rdbms="SQL Server 2005">NCHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">NCHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="NClob" objectid="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10035">
+ <mapping rdbms="Oracle Database 11g">NCLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">NCLOB</mapping>
+ <mapping rdbms="Oracle9i">NCLOB</mapping>
+ <mapping rdbms="SQL Server 2005">NTEXT</mapping>
+ <mapping rdbms="SQL Server 2000">NTEXT</mapping>
+ <mapping rdbms="DB2/390 8">CLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="NTEXT" objectid="LOGDT036">
+ <mapping rdbms="Oracle Database 11g">NCLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">NCLOB</mapping>
+ <mapping rdbms="Oracle9i">NCLOB</mapping>
+ <mapping rdbms="SQL Server 2005">NTEXT</mapping>
+ <mapping rdbms="SQL Server 2000">NTEXT</mapping>
+ <mapping rdbms="DB2/390 8">CLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="NUMERIC" objectid="LOGDT019">
+ <mapping rdbms="Oracle Database 11g">NUMBER, precision, scale</mapping>
+ <mapping rdbms="Oracle Database 10g">NUMBER, precision, scale</mapping>
+ <mapping rdbms="Oracle9i">NUMBER, precision, scale</mapping>
+ <mapping rdbms="SQL Server 2005">NUMERIC, precision, scale</mapping>
+ <mapping rdbms="SQL Server 2000">NUMERIC, precision, scale</mapping>
+ <mapping rdbms="DB2/390 8">NUMERIC, precision, scale</mapping>
+ <mapping rdbms="DB2/390 7">NUMERIC, precision, scale</mapping>
+ <mapping rdbms="DB2/UDB 8.1">NUMERIC, precision, scale</mapping>
+ <mapping rdbms="DB2/UDB 7.1">NUMERIC, precision, scale</mapping>
+ </logicaltype>
+ <logicaltype name="NVARCHAR" objectid="LOGDT037">
+ <mapping rdbms="Oracle Database 11g">NVARCHAR2, size</mapping>
+ <mapping rdbms="Oracle Database 10g">NVARCHAR2, size</mapping>
+ <mapping rdbms="Oracle9i">NVARCHAR2, size</mapping>
+ <mapping rdbms="SQL Server 2005">NVARCHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">NVARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="ORDAUDIO" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10005">
+ <mapping rdbms="Oracle Database 11g">ORDSYS.ORDAudio</mapping>
+ <mapping rdbms="Oracle Database 10g">ORDSYS.ORDAudio</mapping>
+ <mapping rdbms="Oracle9i">ORDSYS.ORDAudio</mapping>
+ <mapping rdbms="SQL Server 2005">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2000">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="ORDDOC" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10009">
+ <mapping rdbms="Oracle Database 11g">ORDSYS.ORDDoc</mapping>
+ <mapping rdbms="Oracle Database 10g">ORDSYS.ORDDoc</mapping>
+ <mapping rdbms="Oracle9i">ORDSYS.ORDDoc</mapping>
+ <mapping rdbms="SQL Server 2005">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2000">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="ORDIMAGE" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10006">
+ <mapping rdbms="Oracle Database 11g">ORDSYS.ORDImage</mapping>
+ <mapping rdbms="Oracle Database 10g">ORDSYS.ORDImage</mapping>
+ <mapping rdbms="Oracle9i">ORDSYS.ORDImage</mapping>
+ <mapping rdbms="SQL Server 2005">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2000">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="ORDIMAGE_SIGNATURE" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10007">
+ <mapping rdbms="Oracle Database 11g">ORDSYS.ORDImageSignature</mapping>
+ <mapping rdbms="Oracle Database 10g">ORDSYS.ORDImageSignature</mapping>
+ <mapping rdbms="Oracle9i">ORDSYS.ORDImageSignature</mapping>
+ <mapping rdbms="SQL Server 2005">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2000">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="ORDVIDEO" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10008">
+ <mapping rdbms="Oracle Database 11g">ORDSYS.ORDVideo</mapping>
+ <mapping rdbms="Oracle Database 10g">ORDSYS.ORDVideo</mapping>
+ <mapping rdbms="Oracle9i">ORDSYS.ORDVideo</mapping>
+ <mapping rdbms="SQL Server 2005">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2000">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="REAL" objectid="LOGDT022">
+ <mapping rdbms="Oracle Database 11g">REAL</mapping>
+ <mapping rdbms="Oracle Database 10g">REAL</mapping>
+ <mapping rdbms="Oracle9i">REAL</mapping>
+ <mapping rdbms="SQL Server 2005">REAL, precision</mapping>
+ <mapping rdbms="SQL Server 2000">REAL, precision</mapping>
+ <mapping rdbms="DB2/390 8">REAL</mapping>
+ <mapping rdbms="DB2/390 7">REAL</mapping>
+ <mapping rdbms="DB2/UDB 8.1">REAL</mapping>
+ <mapping rdbms="DB2/UDB 7.1">REAL</mapping>
+ </logicaltype>
+ <logicaltype name="ROWID" objectid="LOGDT032">
+ <mapping rdbms="Oracle Database 11g">ROWID</mapping>
+ <mapping rdbms="Oracle Database 10g">ROWID</mapping>
+ <mapping rdbms="Oracle9i">ROWID</mapping>
+ <mapping rdbms="SQL Server 2005">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">ROWID</mapping>
+ <mapping rdbms="DB2/390 7">ROWID</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="Raw" objectid="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10040">
+ <mapping rdbms="Oracle Database 11g">RAW, size</mapping>
+ <mapping rdbms="Oracle Database 10g">RAW, size</mapping>
+ <mapping rdbms="Oracle9i">RAW, size</mapping>
+ <mapping rdbms="SQL Server 2005">VARBINARY, size</mapping>
+ <mapping rdbms="SQL Server 2000">VARBINARY, size</mapping>
+ <mapping rdbms="DB2/390 8">VARGRAPHIC, size</mapping>
+ <mapping rdbms="DB2/390 7">VARGRAPHIC, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">VARGRAPHIC, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">VARGRAPHIC, size</mapping>
+ </logicaltype>
+ <logicaltype name="SMALLDATETIME" objectid="LOGDT038">
+ <mapping rdbms="Oracle Database 11g">DATE</mapping>
+ <mapping rdbms="Oracle Database 10g">DATE</mapping>
+ <mapping rdbms="Oracle9i">DATE</mapping>
+ <mapping rdbms="SQL Server 2005">SMALLDATETIME</mapping>
+ <mapping rdbms="SQL Server 2000">SMALLDATETIME</mapping>
+ <mapping rdbms="DB2/390 8">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/390 7">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping>
+ </logicaltype>
+ <logicaltype name="SMALLINT" objectid="LOGDT018">
+ <mapping rdbms="Oracle Database 11g">SMALLINT</mapping>
+ <mapping rdbms="Oracle Database 10g">SMALLINT</mapping>
+ <mapping rdbms="Oracle9i">SMALLINT</mapping>
+ <mapping rdbms="SQL Server 2005">SMALLINT</mapping>
+ <mapping rdbms="SQL Server 2000">SMALLINT</mapping>
+ <mapping rdbms="DB2/390 8">SMALLINT</mapping>
+ <mapping rdbms="DB2/390 7">SMALLINT</mapping>
+ <mapping rdbms="DB2/UDB 8.1">SMALLINT</mapping>
+ <mapping rdbms="DB2/UDB 7.1">SMALLINT</mapping>
+ </logicaltype>
+ <logicaltype name="SMALLMONEY" objectid="LOGDT044">
+ <mapping rdbms="Oracle Database 11g">NUMBER, precision, scale</mapping>
+ <mapping rdbms="Oracle Database 10g">NUMBER, precision, scale</mapping>
+ <mapping rdbms="Oracle9i">NUMBER, precision, scale</mapping>
+ <mapping rdbms="SQL Server 2005">SMALLMONEY</mapping>
+ <mapping rdbms="SQL Server 2000">SMALLMONEY</mapping>
+ <mapping rdbms="DB2/390 8">REAL</mapping>
+ <mapping rdbms="DB2/390 7">REAL</mapping>
+ <mapping rdbms="DB2/UDB 8.1">REAL</mapping>
+ <mapping rdbms="DB2/UDB 7.1">REAL</mapping>
+ </logicaltype>
+ <logicaltype name="SQL_VARIANT" objectid="LOGDT045">
+ <mapping rdbms="Oracle Database 11g">SYS.ANYDATA</mapping>
+ <mapping rdbms="Oracle Database 10g">SYS.ANYDATA</mapping>
+ <mapping rdbms="Oracle9i">SYS.ANYDATA</mapping>
+ <mapping rdbms="SQL Server 2005">SQL_VARIANT</mapping>
+ <mapping rdbms="SQL Server 2000">SQL_VARIANT</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="SYSNAME" objectid="LOGDT039">
+ <mapping rdbms="Oracle Database 11g">VARCHAR2, size</mapping>
+ <mapping rdbms="Oracle Database 10g">VARCHAR2, size</mapping>
+ <mapping rdbms="Oracle9i">VARCHAR2, size</mapping>
+ <mapping rdbms="SQL Server 2005">SYSNAME</mapping>
+ <mapping rdbms="SQL Server 2000">SYSNAME</mapping>
+ <mapping rdbms="DB2/390 8">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="SYS_ANYDATA" objectid="LogDes-F046B719-7D91-3873-3302-38C441683842@LOGDT10010">
+ <mapping rdbms="Oracle Database 11g">SYS.ANYDATA</mapping>
+ <mapping rdbms="Oracle Database 10g">SYS.ANYDATA</mapping>
+ <mapping rdbms="Oracle9i">SYS.ANYDATA</mapping>
+ <mapping rdbms="SQL Server 2005">SQL_VARIANT</mapping>
+ <mapping rdbms="SQL Server 2000">SQL_VARIANT</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="SYS_ANYDATASET" objectid="LogDes-22E251EB-9F6C-8137-56B2-DD4B87DC1E33@LOGDT10030">
+ <mapping rdbms="Oracle Database 11g">SYS.ANYDATASET</mapping>
+ <mapping rdbms="Oracle Database 10g">SYS.ANYDATASET</mapping>
+ <mapping rdbms="Oracle9i">SYS.ANYDATASET</mapping>
+ <mapping rdbms="SQL Server 2005">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2000">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="SYS_ANYTYPE" objectid="LogDes-F046B719-7D91-3873-3302-38C441683842@LOGDT10011">
+ <mapping rdbms="Oracle Database 11g">SYS.ANYTYPE</mapping>
+ <mapping rdbms="Oracle Database 10g">SYS.ANYTYPE</mapping>
+ <mapping rdbms="Oracle9i">SYS.ANYTYPE</mapping>
+ <mapping rdbms="SQL Server 2005">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2000">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <logicaltype name="TEXT" objectid="LOGDT040">
+ <mapping rdbms="Oracle Database 11g">CLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">CLOB</mapping>
+ <mapping rdbms="Oracle9i">CLOB</mapping>
+ <mapping rdbms="SQL Server 2005">TEXT</mapping>
+ <mapping rdbms="SQL Server 2000">TEXT</mapping>
+ <mapping rdbms="DB2/390 8">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="TIMESTAMP WITH LOCAL TIME ZONE" objectid="LOGDT047">
+ <mapping rdbms="Oracle Database 11g">TIMESTAMP WITH LOCAL TIME ZONE, precision</mapping>
+ <mapping rdbms="Oracle Database 10g">TIMESTAMP WITH LOCAL TIME ZONE, precision</mapping>
+ <mapping rdbms="Oracle9i">TIMESTAMP WITH LOCAL TIME ZONE, precision</mapping>
+ <mapping rdbms="SQL Server 2005">DATETIME</mapping>
+ <mapping rdbms="SQL Server 2000">DATETIME</mapping>
+ <mapping rdbms="DB2/390 8">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/390 7">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping>
+ </logicaltype>
+ <logicaltype name="TIMESTAMP WITH TIME ZONE" objectid="LOGDT046">
+ <mapping rdbms="Oracle Database 11g">TIMESTAMP WITH TIME ZONE, precision</mapping>
+ <mapping rdbms="Oracle Database 10g">TIMESTAMP WITH TIME ZONE, precision</mapping>
+ <mapping rdbms="Oracle9i">TIMESTAMP WITH TIME ZONE, precision</mapping>
+ <mapping rdbms="SQL Server 2005">DATETIME</mapping>
+ <mapping rdbms="SQL Server 2000">DATETIME</mapping>
+ <mapping rdbms="DB2/390 8">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/390 7">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping>
+ </logicaltype>
+ <logicaltype name="TINYINT" objectid="LOGDT042">
+ <mapping rdbms="Oracle Database 11g">SMALLINT</mapping>
+ <mapping rdbms="Oracle Database 10g">SMALLINT</mapping>
+ <mapping rdbms="Oracle9i">SMALLINT</mapping>
+ <mapping rdbms="SQL Server 2005">TINYINT</mapping>
+ <mapping rdbms="SQL Server 2000">TINYINT</mapping>
+ <mapping rdbms="DB2/390 8">SMALLINT</mapping>
+ <mapping rdbms="DB2/390 7">SMALLINT</mapping>
+ <mapping rdbms="DB2/UDB 8.1">SMALLINT</mapping>
+ <mapping rdbms="DB2/UDB 7.1">SMALLINT</mapping>
+ </logicaltype>
+ <logicaltype name="Time" objectid="LOGDT014">
+ <mapping rdbms="Oracle Database 11g">DATE</mapping>
+ <mapping rdbms="Oracle Database 10g">DATE</mapping>
+ <mapping rdbms="Oracle9i">DATE</mapping>
+ <mapping rdbms="SQL Server 2005">DATETIME</mapping>
+ <mapping rdbms="SQL Server 2000">DATETIME</mapping>
+ <mapping rdbms="DB2/390 8">TIME</mapping>
+ <mapping rdbms="DB2/390 7">TIME</mapping>
+ <mapping rdbms="DB2/UDB 8.1">TIME</mapping>
+ <mapping rdbms="DB2/UDB 7.1">TIME</mapping>
+ </logicaltype>
+ <logicaltype name="Timestamp" objectid="LOGDT015">
+ <mapping rdbms="Oracle Database 11g">TIMESTAMP, precision</mapping>
+ <mapping rdbms="Oracle Database 10g">TIMESTAMP, precision</mapping>
+ <mapping rdbms="Oracle9i">TIMESTAMP, precision</mapping>
+ <mapping rdbms="SQL Server 2005">DATETIME</mapping>
+ <mapping rdbms="SQL Server 2000">DATETIME</mapping>
+ <mapping rdbms="DB2/390 8">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/390 7">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping>
+ <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping>
+ </logicaltype>
+ <logicaltype name="UNIQUEIDENTIFIER" objectid="LOGDT057">
+ <mapping rdbms="Oracle Database 11g">CHAR, size</mapping>
+ <mapping rdbms="Oracle Database 10g">CHAR, size</mapping>
+ <mapping rdbms="Oracle9i">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2005">UNIQUEIDENTIFIER</mapping>
+ <mapping rdbms="SQL Server 2000">UNIQUEIDENTIFIER</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="URIType" objectid="LOGDT051">
+ <mapping rdbms="Oracle Database 11g">URITYPE</mapping>
+ <mapping rdbms="Oracle Database 10g">URITYPE</mapping>
+ <mapping rdbms="Oracle9i">URITYPE</mapping>
+ <mapping rdbms="SQL Server 2005">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="URowID" objectid="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10041">
+ <mapping rdbms="Oracle Database 11g">UROWID, size</mapping>
+ <mapping rdbms="Oracle Database 10g">UROWID, size</mapping>
+ <mapping rdbms="Oracle9i">UROWID, size</mapping>
+ <mapping rdbms="SQL Server 2005">VARCHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="VARBINARY" objectid="LOGDT041">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">VARBINARY, size</mapping>
+ <mapping rdbms="SQL Server 2000">VARBINARY, size</mapping>
+ <mapping rdbms="DB2/390 8">BLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="VARCHAR" objectid="LOGDT024">
+ <mapping rdbms="Oracle Database 11g">VARCHAR2, size</mapping>
+ <mapping rdbms="Oracle Database 10g">VARCHAR2, size</mapping>
+ <mapping rdbms="Oracle9i">VARCHAR2, size</mapping>
+ <mapping rdbms="SQL Server 2005">VARCHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="VARGRAPHIC" objectid="LOGDT023">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">VARBINARY, size</mapping>
+ <mapping rdbms="SQL Server 2000">VARBINARY, size</mapping>
+ <mapping rdbms="DB2/390 8">VARGRAPHIC, size</mapping>
+ <mapping rdbms="DB2/390 7">VARGRAPHIC, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">VARGRAPHIC, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">VARGRAPHIC, size</mapping>
+ </logicaltype>
+ <logicaltype name="Video" objectid="LOGDT016">
+ <mapping rdbms="Oracle Database 11g">BLOB</mapping>
+ <mapping rdbms="Oracle Database 10g">BLOB</mapping>
+ <mapping rdbms="Oracle9i">BLOB</mapping>
+ <mapping rdbms="SQL Server 2005">IMAGE</mapping>
+ <mapping rdbms="SQL Server 2000">IMAGE</mapping>
+ <mapping rdbms="DB2/390 8">VARGRAPHIC, size</mapping>
+ <mapping rdbms="DB2/390 7">VARGRAPHIC, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">BLOB</mapping>
+ <mapping rdbms="DB2/UDB 7.1">BLOB</mapping>
+ </logicaltype>
+ <logicaltype name="XDBURIType" objectid="LOGDT053">
+ <mapping rdbms="Oracle Database 11g">XDBURITYPE</mapping>
+ <mapping rdbms="Oracle Database 10g">XDBURITYPE</mapping>
+ <mapping rdbms="Oracle9i">XDBURITYPE</mapping>
+ <mapping rdbms="SQL Server 2005">CHAR, size</mapping>
+ <mapping rdbms="SQL Server 2000">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 8">CHAR, size</mapping>
+ <mapping rdbms="DB2/390 7">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping>
+ </logicaltype>
+ <logicaltype name="XMLType" objectid="LOGDT050">
+ <mapping rdbms="Oracle Database 11g">XMLTYPE</mapping>
+ <mapping rdbms="Oracle Database 10g">XMLTYPE</mapping>
+ <mapping rdbms="Oracle9i">XMLTYPE</mapping>
+ <mapping rdbms="SQL Server 2005">XML</mapping>
+ <mapping rdbms="SQL Server 2000">TEXT</mapping>
+ <mapping rdbms="DB2/390 8">CLOB, size</mapping>
+ <mapping rdbms="DB2/390 7">CLOB, size</mapping>
+ <mapping rdbms="DB2/UDB 8.1">XML</mapping>
+ <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping>
+ </logicaltype>
+ <logicaltype name="unknown" objectid="LOGDT017" default="true">
+ <mapping rdbms="Oracle Database 11g">UNKNOWN</mapping>
+ <mapping rdbms="Oracle Database 10g">UNKNOWN</mapping>
+ <mapping rdbms="Oracle9i">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2005">UNKNOWN</mapping>
+ <mapping rdbms="SQL Server 2000">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 8">UNKNOWN</mapping>
+ <mapping rdbms="DB2/390 7">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping>
+ <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping>
+ </logicaltype>
+ <native_to_logical_mappings>
+ <mappings_for_RDBMS_type rdbms_type="Oracle Database 11g">
+ <mapping native_type="BFILE" logicaltype="BFile" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10034" />
+ <mapping native_type="BINARY_DOUBLE" logicaltype="BINARY DOUBLE" log_type_id="LOGDT056" />
+ <mapping native_type="BINARY_FLOAT" logicaltype="BINARY FLOAT" log_type_id="LOGDT055" />
+ <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" />
+ <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" />
+ <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" />
+ <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" />
+ <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" />
+ <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" />
+ <mapping native_type="LONG" logicaltype="Long Char" log_type_id="LogDes-1768A872-F385-FDBA-D95E-0CB63F5908E2@LOGDT10045" />
+ <mapping native_type="LONG RAW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="LONG ROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="LONGROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="NATIONAL CHAR" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NATIONAL CHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NATIONAL CHARACTER" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NATIONAL CHARACTER VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NCHAR" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NCHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NCLOB" logicaltype="NClob" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10035" />
+ <mapping native_type="NUMBER" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="RAW" logicaltype="Raw" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10040" />
+ <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" />
+ <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" />
+ <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" />
+ <mapping native_type="UROWID" logicaltype="URowID" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10041" />
+ <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="VARCHAR2" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ </mappings_for_RDBMS_type>
+ <mappings_for_RDBMS_type rdbms_type="Oracle Database 10g">
+ <mapping native_type="BFILE" logicaltype="BFile" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10034" />
+ <mapping native_type="BINARY_DOUBLE" logicaltype="BINARY DOUBLE" log_type_id="LOGDT056" />
+ <mapping native_type="BINARY_FLOAT" logicaltype="BINARY FLOAT" log_type_id="LOGDT055" />
+ <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" />
+ <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" />
+ <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" />
+ <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" />
+ <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" />
+ <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" />
+ <mapping native_type="LONG" logicaltype="Long Char" log_type_id="LogDes-1768A872-F385-FDBA-D95E-0CB63F5908E2@LOGDT10045" />
+ <mapping native_type="LONG RAW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="LONG ROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="LONGROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="NATIONAL CHAR" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NATIONAL CHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NATIONAL CHARACTER" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NATIONAL CHARACTER VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NCHAR" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NCHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NCLOB" logicaltype="NClob" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10035" />
+ <mapping native_type="NUMBER" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="RAW" logicaltype="Raw" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10040" />
+ <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" />
+ <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" />
+ <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" />
+ <mapping native_type="UROWID" logicaltype="URowID" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10041" />
+ <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="VARCHAR2" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ </mappings_for_RDBMS_type>
+ <mappings_for_RDBMS_type rdbms_type="Oracle9i">
+ <mapping native_type="BFILE" logicaltype="BFile" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10034" />
+ <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" />
+ <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" />
+ <mapping native_type="DBURITYPE" logicaltype="DBURIType" log_type_id="LOGDT054" />
+ <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" />
+ <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" />
+ <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" />
+ <mapping native_type="HTTPURITYPE" logicaltype="HTTPURIType" log_type_id="LOGDT052" />
+ <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" />
+ <mapping native_type="INTERVAL DAY TO SECOND" logicaltype="INTERVAL DAY TO SECOND" log_type_id="LOGDT049" />
+ <mapping native_type="INTERVAL YEAR TO MONTH" logicaltype="INTERVAL YEAR TO MONTH" log_type_id="LOGDT048" />
+ <mapping native_type="LONG" logicaltype="Long Char" log_type_id="LogDes-1768A872-F385-FDBA-D95E-0CB63F5908E2@LOGDT10045" />
+ <mapping native_type="LONG RAW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="LONG ROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="LONGROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" />
+ <mapping native_type="NATIONAL CHAR" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NATIONAL CHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NATIONAL CHARACTER" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NATIONAL CHARACTER VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NCHAR" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NCHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="NCLOB" logicaltype="NClob" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10035" />
+ <mapping native_type="NUMBER" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="NVARCHAR2" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="ORDSYS.ORDAudio" logicaltype="ORDAUDIO" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10005" />
+ <mapping native_type="ORDSYS.ORDDoc" logicaltype="ORDDOC" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10009" />
+ <mapping native_type="ORDSYS.ORDImage" logicaltype="ORDIMAGE" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10006" />
+ <mapping native_type="ORDSYS.ORDImageSignature" logicaltype="ORDIMAGE_SIGNATURE" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10007" />
+ <mapping native_type="ORDSYS.ORDVideo" logicaltype="ORDVIDEO" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10008" />
+ <mapping native_type="RAW" logicaltype="Raw" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10040" />
+ <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" />
+ <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" />
+ <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" />
+ <mapping native_type="SYS.ANYDATA" logicaltype="SYS_ANYDATA" log_type_id="LogDes-F046B719-7D91-3873-3302-38C441683842@LOGDT10010" />
+ <mapping native_type="SYS.ANYDATASET" logicaltype="SYS_ANYDATASET" log_type_id="LogDes-22E251EB-9F6C-8137-56B2-DD4B87DC1E33@LOGDT10030" />
+ <mapping native_type="SYS.ANYTYPE" logicaltype="SYS_ANYTYPE" log_type_id="LogDes-F046B719-7D91-3873-3302-38C441683842@LOGDT10011" />
+ <mapping native_type="TIMESTAMP" logicaltype="Timestamp" log_type_id="LOGDT015" />
+ <mapping native_type="TIMESTAMP WITH LOCAL TIME ZONE" logicaltype="TIMESTAMP WITH LOCAL TIME ZONE" log_type_id="LOGDT047" />
+ <mapping native_type="TIMESTAMP WITH TIME ZONE" logicaltype="TIMESTAMP WITH TIME ZONE" log_type_id="LOGDT046" />
+ <mapping native_type="URITYPE" logicaltype="URIType" log_type_id="LOGDT051" />
+ <mapping native_type="UROWID" logicaltype="URowID" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10041" />
+ <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="VARCHAR2" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="XDBURITYPE" logicaltype="XDBURIType" log_type_id="LOGDT053" />
+ <mapping native_type="XMLTYPE" logicaltype="XMLType" log_type_id="LOGDT050" />
+ </mappings_for_RDBMS_type>
+ <mappings_for_RDBMS_type rdbms_type="SQL Server 2005">
+ <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" />
+ <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" />
+ <mapping native_type="XML" logicaltype="XMLType" log_type_id="LOGDT050" />
+ </mappings_for_RDBMS_type>
+ <mappings_for_RDBMS_type rdbms_type="SQL Server 2000">
+ <mapping native_type="BIGINT" logicaltype="BIGINT" log_type_id="LOGDT027" />
+ <mapping native_type="BINARY" logicaltype="BINARY" log_type_id="LOGDT033" />
+ <mapping native_type="BIT" logicaltype="BIT" log_type_id="LOGDT034" />
+ <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" />
+ <mapping native_type="DATETIME" logicaltype="Datetime" log_type_id="LOGDT008" />
+ <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" />
+ <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" />
+ <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" />
+ <mapping native_type="IMAGE" logicaltype="Image" log_type_id="LOGDT010" />
+ <mapping native_type="INT" logicaltype="Integer" log_type_id="LOGDT011" />
+ <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" />
+ <mapping native_type="MONEY" logicaltype="MONEY" log_type_id="LOGDT043" />
+ <mapping native_type="NCHAR" logicaltype="NCHAR" log_type_id="LOGDT035" />
+ <mapping native_type="NTEXT" logicaltype="NTEXT" log_type_id="LOGDT036" />
+ <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="NVARCHAR" logicaltype="NVARCHAR" log_type_id="LOGDT037" />
+ <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" />
+ <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" />
+ <mapping native_type="SMALLDATETIME" logicaltype="SMALLDATETIME" log_type_id="LOGDT038" />
+ <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" />
+ <mapping native_type="SMALLMONEY" logicaltype="SMALLMONEY" log_type_id="LOGDT044" />
+ <mapping native_type="SQL_VARIANT" logicaltype="SQL_VARIANT" log_type_id="LOGDT045" />
+ <mapping native_type="SYSNAME" logicaltype="SYSNAME" log_type_id="LOGDT039" />
+ <mapping native_type="TEXT" logicaltype="TEXT" log_type_id="LOGDT040" />
+ <mapping native_type="TIMESTAMP" logicaltype="Timestamp" log_type_id="LOGDT015" />
+ <mapping native_type="TINYINT" logicaltype="TINYINT" log_type_id="LOGDT042" />
+ <mapping native_type="UNIQUEIDENTIFIER" logicaltype="UNIQUEIDENTIFIER" log_type_id="LOGDT057" />
+ <mapping native_type="VARBINARY" logicaltype="VARBINARY" log_type_id="LOGDT041" />
+ <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ </mappings_for_RDBMS_type>
+ <mappings_for_RDBMS_type rdbms_type="DB2/390 8">
+ <mapping native_type="GRAPHIC" logicaltype="GRAPHIC" log_type_id="LOGDT031" />
+ </mappings_for_RDBMS_type>
+ <mappings_for_RDBMS_type rdbms_type="DB2/390 7">
+ <mapping native_type="BINARY LARGE OBJECT" logicaltype="BLOB" log_type_id="LOGDT029" />
+ <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" />
+ <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHAR LARGE OBJECT" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHARACTER LARGE OBJECT" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" />
+ <mapping native_type="DBCLOB" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" />
+ <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" />
+ <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" />
+ <mapping native_type="GRAPHIC" logicaltype="GRAPHIC" log_type_id="LOGDT031" />
+ <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" />
+ <mapping native_type="LONG VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="LONG VARGRAPHIC" logicaltype="VARGRAPHIC" log_type_id="LOGDT023" />
+ <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" />
+ <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" />
+ <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" />
+ <mapping native_type="TIME" logicaltype="Time" log_type_id="LOGDT014" />
+ <mapping native_type="TIMESTAMP" logicaltype="Timestamp" log_type_id="LOGDT015" />
+ <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="VARGRAPHIC" logicaltype="VARGRAPHIC" log_type_id="LOGDT023" />
+ </mappings_for_RDBMS_type>
+ <mappings_for_RDBMS_type rdbms_type="DB2/UDB 8.1">
+ <mapping native_type="GRAPHIC" logicaltype="GRAPHIC" log_type_id="LOGDT031" />
+ <mapping native_type="XML" logicaltype="XMLType" log_type_id="LOGDT050" />
+ </mappings_for_RDBMS_type>
+ <mappings_for_RDBMS_type rdbms_type="DB2/UDB 7.1">
+ <mapping native_type="BIGINT" logicaltype="BIGINT" log_type_id="LOGDT027" />
+ <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" />
+ <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" />
+ <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="DATALINK" logicaltype="DATALINK" log_type_id="LOGDT030" />
+ <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" />
+ <mapping native_type="DBCLOB" logicaltype="CLOB" log_type_id="LOGDT028" />
+ <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" />
+ <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" />
+ <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" />
+ <mapping native_type="GRAPHIC" logicaltype="GRAPHIC" log_type_id="LOGDT031" />
+ <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" />
+ <mapping native_type="LONG VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="LONG VARGRAPHIC" logicaltype="VARGRAPHIC" log_type_id="LOGDT023" />
+ <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" />
+ <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" />
+ <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" />
+ <mapping native_type="TIME" logicaltype="Time" log_type_id="LOGDT014" />
+ <mapping native_type="TIMESTAMP" logicaltype="Timestamp" log_type_id="LOGDT015" />
+ <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" />
+ <mapping native_type="VARGRAPHIC" logicaltype="VARGRAPHIC" log_type_id="LOGDT023" />
+ </mappings_for_RDBMS_type>
+ </native_to_logical_mappings>
+ <ud_native_db_types />
+</logtypes> \ No newline at end of file
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseComments.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseComments.pgsql
new file mode 100644
index 00000000..75d3e053
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseComments.pgsql
@@ -0,0 +1,1193 @@
+-- $Id: TestManagerDatabaseComments.pgsql $
+--- @file
+-- Autogenerated from TestManagerDatabaseInit.pgsql. Do not edit!
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+COMMENT ON COLUMN SystemLog.tsCreated IS
+ 'When this was logged.';
+
+COMMENT ON COLUMN SystemLog.sEvent IS
+ 'The event type.
+This is a 8 character string identifier so that we don''t need to change
+some enum type everytime we introduce a new event type.';
+
+COMMENT ON COLUMN SystemLog.sLogText IS
+ 'The log text.';
+
+COMMENT ON TABLE Users IS
+ 'Test manager users.
+
+This is mainly for doing simple access checks before permitting access to
+the test manager. This needs to be coordinated with
+apache/ldap/Oracle-Single-Sign-On.
+
+The main purpose, though, is for tracing who changed the test config and
+analysis data.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp.';
+
+COMMENT ON COLUMN Users.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN Users.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN Users.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN Users.sUsername IS
+ 'User name.';
+
+COMMENT ON COLUMN Users.sEmail IS
+ 'The email address of the user.';
+
+COMMENT ON COLUMN Users.sFullName IS
+ 'The full name.';
+
+COMMENT ON COLUMN Users.sLoginName IS
+ 'The login name used by apache.';
+
+COMMENT ON COLUMN Users.fReadOnly IS
+ 'Read access only.';
+
+COMMENT ON TABLE GlobalResources IS
+ 'Global resource configuration.
+
+For example an iSCSI target.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp.';
+
+COMMENT ON COLUMN GlobalResources.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN GlobalResources.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN GlobalResources.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN GlobalResources.sName IS
+ 'The name of the resource.';
+
+COMMENT ON COLUMN GlobalResources.sDescription IS
+ 'Optional resource description.';
+
+COMMENT ON COLUMN GlobalResources.fEnabled IS
+ 'Indicates whether this resource is currently enabled (online).';
+
+COMMENT ON TABLE BuildSources IS
+ 'Build sources.
+
+This is used by a scheduling group to select builds and the default
+Validation Kit from the Builds table.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp.
+
+@todo Any better way of representing this so we could more easily
+ join/whatever when searching for builds?';
+
+COMMENT ON COLUMN BuildSources.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN BuildSources.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN BuildSources.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN BuildSources.sName IS
+ 'The name of the build source.';
+
+COMMENT ON COLUMN BuildSources.sDescription IS
+ 'Description.';
+
+COMMENT ON COLUMN BuildSources.sProduct IS
+ 'Which product.
+ASSUME that it is okay to limit a build source to a single product.';
+
+COMMENT ON COLUMN BuildSources.sBranch IS
+ 'Which branch.
+ASSUME that it is okay to limit a build source to a branch.';
+
+COMMENT ON COLUMN BuildSources.asTypes IS
+ 'Build types to include, all matches if NULL.
+@todo Weighting the types would be nice in a later version.';
+
+COMMENT ON COLUMN BuildSources.asOsArches IS
+ 'Array of the ''sOs.sCpuArch'' to match, all matches if NULL.
+See KBUILD_OSES in kBuild for a list of standard target OSes, and
+KBUILD_ARCHES for a list of standard architectures.
+
+@remarks See marks on ''os-agnostic'' and ''noarch'' in BuildCategories.';
+
+COMMENT ON COLUMN BuildSources.iFirstRevision IS
+ 'The first subversion tree revision to match, no lower limit if NULL.';
+
+COMMENT ON COLUMN BuildSources.iLastRevision IS
+ 'The last subversion tree revision to match, no upper limit if NULL.';
+
+COMMENT ON COLUMN BuildSources.cSecMaxAge IS
+ 'The maximum age of the builds in seconds, unlimited if NULL.';
+
+COMMENT ON TABLE TestCases IS
+ 'Test case configuration.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp.';
+
+COMMENT ON COLUMN TestCases.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestCases.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestCases.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN TestCases.sName IS
+ 'The name of the test case.';
+
+COMMENT ON COLUMN TestCases.sDescription IS
+ 'Optional test case description.';
+
+COMMENT ON COLUMN TestCases.fEnabled IS
+ 'Indicates whether this test case is currently enabled.';
+
+COMMENT ON COLUMN TestCases.cSecTimeout IS
+ 'Default test case timeout given in seconds.';
+
+COMMENT ON COLUMN TestCases.sTestBoxReqExpr IS
+ 'Default TestBox requirement expression (python boolean expression).
+All the scheduler properties are available for use with the same names
+as in that table.
+If NULL everything matches.';
+
+COMMENT ON COLUMN TestCases.sBuildReqExpr IS
+ 'Default build requirement expression (python boolean expression).
+The following build properties are available: sProduct, sBranch,
+sType, asOsArches, sVersion, iRevision, uidAuthor and idBuild.
+If NULL everything matches.';
+
+COMMENT ON COLUMN TestCases.sBaseCmd IS
+ 'The base command.
+String suitable for executing in bourne shell with space as separator
+(IFS). References to @BUILD_BINARIES@ will be replaced WITH the content
+of the Builds(sBinaries) field.';
+
+COMMENT ON COLUMN TestCases.sTestSuiteZips IS
+ 'Comma separated list of test suite zips (or tars) that the testbox will
+need to download and expand prior to testing.
+If NULL the current test suite of the scheduling group will be used (the
+scheduling group will have an optional test suite build queue associated
+with it). The current test suite can also be referenced by
+@VALIDATIONKIT_ZIP@ in case more downloads are required. Files may also be
+uploaded to the test manager download area, in which case the
+@DOWNLOAD_BASE_URL@ prefix can be used to refer to this area.';
+
+COMMENT ON TABLE TestCaseArgs IS
+ 'Test case argument list variations.
+
+For example, we have a test case that does a set of tests on a virtual
+machine. To get better code/feature coverage of this testcase we wish to
+run it with different guest hardware configuration. The test case may do
+the same stuff, but the guest OS as well as the VMM may react differently to
+the hardware configurations and uncover issues in the VMM, device emulation
+or other places.
+
+Typical hardware variations are:
+ - guest memory size (RAM),
+ - guest video memory size (VRAM),
+ - virtual CPUs / cores / threads,
+ - virtual chipset
+ - virtual network interface card (NIC)
+ - USB 1.1, USB 2.0, no USB
+
+The TM web UI will help the user create a reasonable set of permutations
+of these parameters, the user specifies a maximum and the TM uses certain
+rules together with random selection to generate the desired number. The
+UI will also help suggest fitting testbox requirements according to the
+RAM/VRAM sizes and the virtual CPU counts. The user may then make
+adjustments to the suggestions before commit them.
+
+Alternatively, the user may also enter all the permutations without any
+help from the UI.
+
+Note! All test cases has at least one entry in this table, even if it is
+empty, because testbox requirements are specified thru this.
+
+Querying the valid parameter lists for a testase this way:
+ SELECT * ... WHERE idTestCase = TestCases.idTestCase
+ AND tsExpire > <when>
+ AND tsEffective <= <when>;
+
+Querying the valid parameter list for the latest generation can be
+simplified by just checking tsExpire date:
+ SELECT * ... WHERE idTestCase = TestCases.idTestCase
+ AND tsExpire == TIMESTAMP WITH TIME ZONE ''infinity'';
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp.';
+
+COMMENT ON COLUMN TestCaseArgs.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestCaseArgs.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestCaseArgs.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN TestCaseArgs.sArgs IS
+ 'The additional arguments.
+String suitable for bourne shell style argument parsing with space as
+separator (IFS). References to @BUILD_BINARIES@ will be replaced with
+the content of the Builds(sBinaries) field.';
+
+COMMENT ON COLUMN TestCaseArgs.cSecTimeout IS
+ 'Optional test case timeout given in seconds.
+If NULL, the TestCases.cSecTimeout field is used instead.';
+
+COMMENT ON COLUMN TestCaseArgs.sTestBoxReqExpr IS
+ 'Additional TestBox requirement expression (python boolean expression).
+All the scheduler properties are available for use with the same names
+as in that table. This is checked after first checking the requirements
+in the TestCases.sTestBoxReqExpr field.';
+
+COMMENT ON COLUMN TestCaseArgs.sBuildReqExpr IS
+ 'Additional build requirement expression (python boolean expression).
+The following build properties are available: sProduct, sBranch,
+sType, asOsArches, sVersion, iRevision, uidAuthor and idBuild. This is
+checked after first checking the requirements in the
+TestCases.sBuildReqExpr field.';
+
+COMMENT ON COLUMN TestCaseArgs.cGangMembers IS
+ 'Number of testboxes required (gang scheduling).';
+
+COMMENT ON COLUMN TestCaseArgs.sSubName IS
+ 'Optional variation sub-name.';
+
+COMMENT ON INDEX TestCaseArgsLookupIdx IS
+ 'The arguments are part of the primary key for several reasons.
+No duplicate argument lists (makes no sense - if you want to prioritize
+argument lists, we add that explicitly). This may hopefully enable us
+to more easily check coverage later on, even when the test case is
+reconfigured with more/less permutations.';
+
+COMMENT ON TABLE TestCaseDeps IS
+ 'Test case dependencies (N:M)
+
+This effect build selection. The build must have passed all runs of the
+given prerequisite testcase (idTestCasePreReq) and executed at a minimum one
+argument list variation.
+
+This should also affect scheduling order, if possible at least one
+prerequisite testcase variation should be place before the specific testcase
+in the scheduling queue.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN TestCaseDeps.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestCaseDeps.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestCaseDeps.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON TABLE TestCaseGlobalRsrcDeps IS
+ 'Test case dependencies on global resources (N:M)
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN TestCaseGlobalRsrcDeps.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestCaseGlobalRsrcDeps.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestCaseGlobalRsrcDeps.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON TABLE TestGroups IS
+ 'Test Group - A collection of test cases.
+
+This is for simplifying test configuration by working with a few groups
+instead of a herd of individual testcases. It may also be used for creating
+test suites for certain areas (like guest additions) or tasks (like
+performance measurements).
+
+A test case can be member of any number of test groups.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN TestGroups.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestGroups.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestGroups.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN TestGroups.sName IS
+ 'The name of the scheduling group.';
+
+COMMENT ON COLUMN TestGroups.sDescription IS
+ 'Optional group description.';
+
+COMMENT ON TABLE TestGroupMembers IS
+ 'The N:M relationship between test case configurations and test groups.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN TestGroupMembers.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestGroupMembers.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestGroupMembers.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN TestGroupMembers.iSchedPriority IS
+ 'Test case scheduling priority.
+Higher number causes the test case to be run more frequently.
+@sa SchedGroupMembers.iSchedPriority, TestBoxesInSchedGroups.iSchedPriority
+@todo Not sure we want to keep this...';
+
+COMMENT ON TABLE SchedGroups IS
+ 'Scheduling group (aka. testbox partitioning) configuration.
+
+A testbox is associated with exactly one scheduling group. This association
+can be changed, of course. If we (want to) retire a group which still has
+testboxes associated with it, these will be moved to the ''default'' group.
+
+The TM web UI will make sure that a testbox is always in a group and that
+the default group cannot be deleted.
+
+A scheduling group combines several things:
+ - A selection of builds to test (via idBuildSrc).
+ - A collection of test groups to test with (via SchedGroupMembers).
+ - A set of testboxes to test on (via TestBoxes.idSchedGroup).
+
+In additions there is an optional source of fresh test suite builds (think
+VBoxTestSuite) as well as scheduling options.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN SchedGroups.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN SchedGroups.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN SchedGroups.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)
+@note This is NULL for the default group.';
+
+COMMENT ON COLUMN SchedGroups.sName IS
+ 'The name of the scheduling group.';
+
+COMMENT ON COLUMN SchedGroups.sDescription IS
+ 'Optional group description.';
+
+COMMENT ON COLUMN SchedGroups.fEnabled IS
+ 'Indicates whether this group is currently enabled.';
+
+COMMENT ON COLUMN SchedGroups.enmScheduler IS
+ 'The scheduler to use.
+This is for when we later desire different scheduling that the best
+effort stuff provided by the initial implementation.';
+
+COMMENT ON COLUMN SchedGroups.sComment IS
+ 'The Validation Kit build source (@VALIDATIONKIT_ZIP@).
+Non-unique foreign key: BuildSources(idBuildSrc)';
+
+COMMENT ON TABLE SchedGroupMembers IS
+ 'N:M relationship between scheduling groups and test groups.
+
+Several scheduling parameters are associated with this relationship.
+
+The test group dependency (idTestGroupPreReq) can be used in the same way as
+TestCaseDeps.idTestCasePreReq, only here on test group level. This means it
+affects the build selection. The builds needs to have passed all test runs
+the prerequisite test group and done at least one argument variation of each
+test case in it.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN SchedGroupMembers.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN SchedGroupMembers.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN SchedGroupMembers.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN SchedGroupMembers.iSchedPriority IS
+ 'The scheduling priority of the test group.
+Higher number causes the test case to be run more frequently.
+@sa TestGroupMembers.iSchedPriority, TestBoxesInSchedGroups.iSchedPriority';
+
+COMMENT ON COLUMN SchedGroupMembers.bmHourlySchedule IS
+ 'When during the week this group is allowed to start running, NULL means
+there are no constraints.
+Each bit in the bitstring represents one hour, with bit 0 indicating the
+midnight hour on a monday.';
+
+COMMENT ON TABLE TestBoxStrTab IS
+ 'String table for the test boxes.
+
+This is a string cache for all string members in TestBoxes except the name.
+The rational is to avoid duplicating large strings like sReport when the
+testbox reports a new cMbScratch value or the box when the test sheriff
+sends a reboot command or similar.
+
+At the time this table was introduced, we had 400558 TestBoxes rows, where
+the SUM(LENGTH(sReport)) was 993MB. There were really just 1066 distinct
+sReport values, with a total length of 0x3 MB.
+
+Nothing is ever deleted from this table.
+
+@note Should use a stored procedure to query/insert a string.
+
+
+TestBox stats prior to conversion:
+ SELECT COUNT(*) FROM TestBoxes: 400558 rows
+ SELECT pg_total_relation_size(''TestBoxes''): 740794368 bytes (706 MB)
+ Average row cost: 740794368 / 400558 = 1849 bytes/row
+
+After conversion:
+ SELECT COUNT(*) FROM TestBoxes: 400558 rows
+ SELECT pg_total_relation_size(''TestBoxes''): 144375808 bytes (138 MB)
+ SELECT COUNT(idStr) FROM TestBoxStrTab: 1292 rows
+ SELECT pg_total_relation_size(''TestBoxStrTab''): 5709824 bytes (5.5 MB)
+ (144375808 + 5709824) / 740794368 = 20 %
+ Average row cost boxes: 144375808 / 400558 = 360 bytes/row
+ Average row cost strings: 5709824 / 1292 = 4420 bytes/row';
+
+COMMENT ON COLUMN TestBoxStrTab.sValue IS
+ 'The string value.';
+
+COMMENT ON COLUMN TestBoxStrTab.tsCreated IS
+ 'Creation time stamp.';
+
+COMMENT ON TYPE TestBoxCmd_T IS
+ 'Testbox commands.';
+
+COMMENT ON TYPE LomKind_T IS
+ 'The kind of lights out management on a testbox.';
+
+COMMENT ON TABLE TestBoxes IS
+ 'Testbox configurations.
+
+The testboxes are identified by IP and the system UUID if available. Should
+the IP change, the testbox will be refused at sign on and the testbox
+sheriff will have to update it''s IP.
+
+@todo Implement the UUID stuff. Get it from DMI, UEFI or whereever.
+ Mismatching needs to be logged somewhere...
+
+To query the currently valid configuration:
+ SELECT ... WHERE id = idTestBox AND tsExpire = TIMESTAMP WITH TIME ZONE ''infinity'';
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN TestBoxes.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestBoxes.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestBoxes.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+When modified automatically by the testbox, NULL is used.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN TestBoxes.uuidSystem IS
+ 'The system or firmware UUID.
+This uniquely identifies the testbox when talking to the server. After
+SIGNON though, the testbox will also provide idTestBox and ip to
+establish its identity beyond doubt.';
+
+COMMENT ON COLUMN TestBoxes.sName IS
+ 'The testbox name.
+Usually similar to the DNS name.';
+
+COMMENT ON COLUMN TestBoxes.fEnabled IS
+ 'Indicates whether this testbox is enabled.
+A testbox gets disabled when we''re doing maintenance, debugging a issue
+that happens only on that testbox, or some similar stuff. This is an
+alternative to deleting the testbox.';
+
+COMMENT ON COLUMN TestBoxes.enmLomKind IS
+ 'The kind of lights-out-management.';
+
+COMMENT ON COLUMN TestBoxes.lCpuRevision IS
+ 'Number identifying the CPU family/model/stepping/whatever.
+For x86 and AMD64 type CPUs, this will on the following format:
+ (EffFamily << 24) | (EffModel << 8) | Stepping.';
+
+COMMENT ON COLUMN TestBoxes.cCpus IS
+ 'Number of CPUs, CPU cores and CPU threads.';
+
+COMMENT ON COLUMN TestBoxes.fCpuHwVirt IS
+ 'Set if capable of hardware virtualization.';
+
+COMMENT ON COLUMN TestBoxes.fCpuNestedPaging IS
+ 'Set if capable of nested paging.';
+
+COMMENT ON COLUMN TestBoxes.fCpu64BitGuest IS
+ 'Set if CPU capable of 64-bit (VBox) guests.';
+
+COMMENT ON COLUMN TestBoxes.fChipsetIoMmu IS
+ 'Set if chipset with usable IOMMU (VT-d / AMD-Vi).';
+
+COMMENT ON COLUMN TestBoxes.fRawMode IS
+ 'Set if the test box does raw-mode tests.';
+
+COMMENT ON COLUMN TestBoxes.cMbMemory IS
+ 'The (approximate) memory size in megabytes (rounded down to nearest 4 MB).';
+
+COMMENT ON COLUMN TestBoxes.cMbScratch IS
+ 'The amount of scratch space in megabytes (rounded down to nearest 64 MB).';
+
+COMMENT ON COLUMN TestBoxes.iTestBoxScriptRev IS
+ 'The testbox script revision number, serves the purpose of a version number.
+Probably good to have when scheduling upgrades as well for status purposes.';
+
+COMMENT ON COLUMN TestBoxes.iPythonHexVersion IS
+ 'The python sys.hexversion (layed out as of 2.7).
+Good to know which python versions we need to support.';
+
+COMMENT ON COLUMN TestBoxes.enmPendingCmd IS
+ 'Pending command.
+@note We put it here instead of in TestBoxStatuses to get history.';
+
+COMMENT ON INDEX TestBoxesUuidIdx IS
+ 'Nested paging requires hardware virtualization.';
+
+COMMENT ON TABLE TestBoxesInSchedGroups IS
+ 'N:M relationship between test boxes and scheduling groups.
+
+We associate a priority with this relationship.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN TestBoxesInSchedGroups.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestBoxesInSchedGroups.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestBoxesInSchedGroups.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN TestBoxesInSchedGroups.iSchedPriority IS
+ 'The scheduling priority of the scheduling group for the test box.
+Higher number causes the scheduling group to be serviced more frequently.
+@sa TestGroupMembers.iSchedPriority, SchedGroups.iSchedPriority';
+
+COMMENT ON TABLE FailureCategories IS
+ 'Failure categories.
+
+This is for organizing the failure reasons.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN FailureCategories.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN FailureCategories.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN FailureCategories.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN FailureCategories.sShort IS
+ 'The short category description.
+For combo boxes and other selection lists.';
+
+COMMENT ON COLUMN FailureCategories.sFull IS
+ 'Full description
+For cursor-over-poppups for instance.';
+
+COMMENT ON TABLE FailureReasons IS
+ 'Failure reasons.
+
+When analysing a test failure, the testbox sheriff will try assign a fitting
+reason for the failure. This table is here to help the sheriff in his/hers
+job as well as developers looking checking if their changes affected the
+test results in any way.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN FailureReasons.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN FailureReasons.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN FailureReasons.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN FailureReasons.sShort IS
+ 'The short failure description.
+For combo boxes and other selection lists.';
+
+COMMENT ON COLUMN FailureReasons.sFull IS
+ 'Full failure description.';
+
+COMMENT ON COLUMN FailureReasons.iTicket IS
+ 'Ticket number in the primary bugtracker.';
+
+COMMENT ON COLUMN FailureReasons.asUrls IS
+ 'Other URLs to reports or discussions of the observed symptoms.';
+
+COMMENT ON TABLE TestResultFailures IS
+ 'This is for tracking/discussing test result failures.
+
+The rational for putting this is a separate table is that we need history on
+this while TestResults does not.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN TestResultFailures.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN TestResultFailures.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN TestResultFailures.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN TestResultFailures.sComment IS
+ 'Optional comment.';
+
+COMMENT ON TABLE BuildBlacklist IS
+ 'Table used to blacklist sets of builds.
+
+The best usage example is a VMM developer realizing that a change causes the
+host to panic, hang, or otherwise misbehave. To prevent the testbox sheriff
+from repeatedly having to reboot testboxes, the builds gets blacklisted
+until there is a working build again. This may mean adding an open ended
+blacklist spec and then updating it with the final revision number once the
+fix has been committed.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.
+
+@todo Would be nice if we could replace the text strings below with a set of
+ BuildCategories, or sore it in any other way which would enable us to
+ do a negative join with build category... The way it is specified
+ now, it looks like we have to open a cursor of prospecitve builds and
+ filter then thru this table one by one.
+
+ Any better representation is welcome, but this is low prioirty for
+ now, as it''s relatively easy to change this later one.';
+
+COMMENT ON COLUMN BuildBlacklist.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN BuildBlacklist.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN BuildBlacklist.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)';
+
+COMMENT ON COLUMN BuildBlacklist.sProduct IS
+ 'Which product.
+ASSUME that it is okay to limit a blacklisting to a single product.';
+
+COMMENT ON COLUMN BuildBlacklist.sBranch IS
+ 'Which branch.
+ASSUME that it is okay to limit a blacklisting to a branch.';
+
+COMMENT ON COLUMN BuildBlacklist.asTypes IS
+ 'Build types to include, all matches if NULL.';
+
+COMMENT ON COLUMN BuildBlacklist.asOsArches IS
+ 'Array of the ''sOs.sCpuArch'' to match, all matches if NULL.
+See KBUILD_OSES in kBuild for a list of standard target OSes, and
+KBUILD_ARCHES for a list of standard architectures.
+
+@remarks See marks on ''os-agnostic'' and ''noarch'' in BuildCategories.';
+
+COMMENT ON COLUMN BuildBlacklist.iFirstRevision IS
+ 'The first subversion tree revision to blacklist.';
+
+COMMENT ON COLUMN BuildBlacklist.iLastRevision IS
+ 'The last subversion tree revision to blacklist, no upper limit if NULL.';
+
+COMMENT ON TABLE BuildCategories IS
+ 'Build categories.
+
+The purpose of this table is saving space in the Builds table and hopefully
+speed things up when selecting builds as well (compared to selecting on 4
+text fields in the much larger Builds table).
+
+Insert only table, no update, no delete. History is not needed.';
+
+COMMENT ON COLUMN BuildCategories.sProduct IS
+ 'Product.
+The product name. For instance ''VBox'' or ''VBoxTestSuite''.';
+
+COMMENT ON COLUMN BuildCategories.sRepository IS
+ 'The version control repository name.';
+
+COMMENT ON COLUMN BuildCategories.sBranch IS
+ 'The branch name (in the version control system).';
+
+COMMENT ON COLUMN BuildCategories.sType IS
+ 'The build type.
+See KBUILD_BLD_TYPES in kBuild for a list of standard build types.';
+
+COMMENT ON COLUMN BuildCategories.asOsArches IS
+ 'Array of the ''sOs.sCpuArch'' supported by the build.
+See KBUILD_OSES in kBuild for a list of standard target OSes, and
+KBUILD_ARCHES for a list of standard architectures.
+
+@remarks ''os-agnostic'' is used if the build doesn''t really target any
+ specific OS or if it targets all applicable OSes.
+ ''noarch'' is used if the build is architecture independent or if
+ all applicable architectures are handled.
+ Thus, ''os-agnostic.noarch'' will run on all build boxes.
+
+@note The array shall be sorted ascendingly to prevent unnecessary duplicates!';
+
+COMMENT ON TABLE Builds IS
+ 'The builds table contains builds from the tinderboxes and oaccasionally from
+developers.
+
+The tinderbox side could be fed by a batch job enumerating the build output
+directories every so often, looking for new builds. Or we could query them
+from the tinderbox database. Yet another alternative is making the
+tinderbox server or client side software inform us about all new builds.
+
+The developer builds are entered manually thru the TM web UI. They are used
+for subjecting new code to some larger scale testing before commiting,
+enabling, or merging a private branch.
+
+The builds are being selected from this table by the via the build source
+specification that SchedGroups.idBuildSrc and
+SchedGroups.idBuildSrcTestSuite links to.
+
+@remarks This table stores history. Never update or delete anything. The
+ equivalent of deleting is done by setting the ''tsExpire'' field to
+ current_timestamp. To select the currently valid entries use
+ tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.';
+
+COMMENT ON COLUMN Builds.tsCreated IS
+ 'When this build was created or entered into the database.
+This remains unchanged';
+
+COMMENT ON COLUMN Builds.tsEffective IS
+ 'When this row starts taking effect (inclusive).';
+
+COMMENT ON COLUMN Builds.tsExpire IS
+ 'When this row stops being tsEffective (exclusive).';
+
+COMMENT ON COLUMN Builds.uidAuthor IS
+ 'The user id of the one who created/modified this entry.
+Non-unique foreign key: Users(uid)
+@note This is NULL if added by a batch job / tinderbox.';
+
+COMMENT ON COLUMN Builds.iRevision IS
+ 'The subversion tree revision of the build.';
+
+COMMENT ON COLUMN Builds.sVersion IS
+ 'The product version number (suitable for RTStrVersionCompare).';
+
+COMMENT ON COLUMN Builds.sLogUrl IS
+ 'The link to the tinderbox log of this build.';
+
+COMMENT ON COLUMN Builds.sBinaries IS
+ 'Comma separated list of binaries.
+The binaries have paths relative to the TESTBOX_PATH_BUILDS or full URLs.';
+
+COMMENT ON COLUMN Builds.fBinariesDeleted IS
+ 'Set when the binaries gets deleted by the build quota script.';
+
+COMMENT ON TABLE VcsRevisions IS
+ 'This table is for translating build revisions into commit details.
+
+For graphs and test results, it would be useful to translate revisions into
+dates and maybe provide commit message and the committer.
+
+Data is entered exclusively thru one or more batch jobs, so no internal
+authorship needed. Also, since we''re mirroring data from external sources
+here, the batch job is allowed to update/replace existing records.
+
+@todo We we could collect more info from the version control systems, if we
+ believe it''s useful and can be presented in a reasonable manner.
+ Getting a list of affected files would be simple (requires
+ a separate table with a M:1 relationship to this table), or try
+ associate a commit to a branch.';
+
+COMMENT ON COLUMN VcsRevisions.sRepository IS
+ 'The version control tree name.';
+
+COMMENT ON COLUMN VcsRevisions.iRevision IS
+ 'The version control tree revision number.';
+
+COMMENT ON COLUMN VcsRevisions.tsCreated IS
+ 'When the revision was created (committed).';
+
+COMMENT ON COLUMN VcsRevisions.sAuthor IS
+ 'The name of the committer.
+@note Not to be confused with uidAuthor and test manager users.';
+
+COMMENT ON COLUMN VcsRevisions.sMessage IS
+ 'The commit message.';
+
+COMMENT ON TABLE TestResultStrTab IS
+ 'String table for the test results.
+
+This is a string cache for value names, test names and possible more, that
+is frequently repated in the test results record for each test run. The
+purpose is not only to save space, but to make datamining queries faster by
+giving them integer fields to work on instead of text fields. There may
+possibly be some benefits on INSERT as well as there are only integer
+indexes.
+
+Nothing is ever deleted from this table.
+
+@note Should use a stored procedure to query/insert a string.';
+
+COMMENT ON COLUMN TestResultStrTab.sValue IS
+ 'The string value.';
+
+COMMENT ON COLUMN TestResultStrTab.tsCreated IS
+ 'Creation time stamp.';
+
+COMMENT ON TYPE TestStatus_T IS
+ 'The status of a test (set / result).';
+
+COMMENT ON TABLE TestResults IS
+ 'Test results - a recursive bundle of joy!
+
+A test case will be created when the testdriver calls reporter.testStart and
+concluded with reporter.testDone. The testdriver (or it subordinates) can
+use these methods to create nested test results. For IPRT based test cases,
+RTTestCreate, RTTestInitAndCreate and RTTestSub will both create new test
+result records, where as RTTestSubDone, RTTestSummaryAndDestroy and
+RTTestDestroy will conclude records.
+
+By concluding is meant updating the status. When the test driver reports
+success, we check it against reported results. (paranoia strikes again!)
+
+Nothing is ever deleted from this table.
+
+@note As seen below, several other tables associate data with a
+ test result, and the top most test result is referenced by the
+ test set.';
+
+COMMENT ON COLUMN TestResults.tsCreated IS
+ 'Creation time stamp. This may also be the timestamp of when the test started.';
+
+COMMENT ON COLUMN TestResults.tsElapsed IS
+ 'The elapsed time for this test.
+This is either reported by the directly (with some sanity checking) or
+calculated (current_timestamp - created_ts).
+@todo maybe use a nanosecond field here, check with what';
+
+COMMENT ON COLUMN TestResults.cErrors IS
+ 'The error count.';
+
+COMMENT ON COLUMN TestResults.enmStatus IS
+ 'The test status.';
+
+COMMENT ON COLUMN TestResults.iNestingDepth IS
+ 'Nesting depth.';
+
+COMMENT ON TABLE TestResultValues IS
+ 'Test result values.
+
+A testdriver or subordinate may report a test value via
+reporter.testValue(), while IPRT based test will use RTTestValue and
+associates.
+
+This is an insert only table, no deletes, no updates.';
+
+COMMENT ON COLUMN TestResultValues.tsCreated IS
+ 'Creation time stamp.';
+
+COMMENT ON COLUMN TestResultValues.lValue IS
+ 'The value.';
+
+COMMENT ON COLUMN TestResultValues.iUnit IS
+ 'The unit.
+@todo This is currently not defined properly. Will fix/correlate this
+ with the other places we use unit (IPRT/testdriver/VMMDev).';
+
+COMMENT ON TABLE TestResultFiles IS
+ 'Test result files.
+
+A testdriver or subordinate may report a file by using
+reporter.addFile() or reporter.addLogFile().
+
+The files stored here as well as the primary log file will be processed by a
+batch job and compressed if considered compressable. Thus, TM will look for
+files with a .gz/.bz2 suffix first and then without a suffix.
+
+This is an insert only table, no deletes, no updates.';
+
+COMMENT ON COLUMN TestResultFiles.tsCreated IS
+ 'Creation time stamp.';
+
+COMMENT ON INDEX TestResultFilesIdx IS
+ 'The mime type for the file.
+For instance: ''text/plain'',
+ ''image/png'',
+ ''video/webm'',
+ ''text/xml''';
+
+COMMENT ON TABLE TestResultMsgs IS
+ 'Test result message.
+
+A testdriver or subordinate may report a message via the sDetails parameter
+of the reporter.testFailure() method, while IPRT test cases will use
+RTTestFailed, RTTestPrintf and their friends. For RTTestPrintf, we will
+ignore the more verbose message levels since these can also be found in one
+of the logs.
+
+This is an insert only table, no deletes, no updates.';
+
+COMMENT ON COLUMN TestResultMsgs.tsCreated IS
+ 'Creation time stamp.';
+
+COMMENT ON COLUMN TestResultMsgs.enmLevel IS
+ 'The message level.';
+
+COMMENT ON TABLE TestSets IS
+ 'Test sets / Test case runs.
+
+This is where we collect data about test runs.
+
+@todo Not entirely sure where the ''test set'' term came from. Consider
+ finding something more appropriate.';
+
+COMMENT ON COLUMN TestSets.tsConfig IS
+ 'The test config timestamp, used when reading test config.';
+
+COMMENT ON COLUMN TestSets.tsCreated IS
+ 'When this test set was scheduled.
+idGenTestBox is valid at this point.';
+
+COMMENT ON COLUMN TestSets.tsDone IS
+ 'When this test completed, i.e. testing stopped. This should only be set once.';
+
+COMMENT ON COLUMN TestSets.enmStatus IS
+ 'The current status.';
+
+COMMENT ON COLUMN TestSets.sBaseFilename IS
+ 'The base filename used for storing files related to this test set.
+This is a path relative to wherever TM is dumping log files. In order
+to not become a file system test case, we will try not to put too many
+hundred thousand files in a directory. A simple first approach would
+be to just use the current date (tsCreated) like this:
+ TM_FILE_DIR/year/month/day/TestSets.idTestSet
+
+The primary log file for the test is this name suffixed by ''.log''.
+
+The files in the testresultfile table gets their full names like this:
+ TM_FILE_DIR/sBaseFilename-testresultfile.id-TestResultStrTab(testresultfile.idStrFilename)
+
+@remarks We store this explicitly in case we change the directly layout
+ at some later point.';
+
+COMMENT ON COLUMN TestSets.iGangMemberNo IS
+ 'The gang member number number, 0 is the leader.';
+
+COMMENT ON INDEX TestSetsGangIdx IS
+ 'The test set of the gang leader, NULL if no gang involved.
+@note This is set by the gang leader as well, so that we can find all
+ gang members by WHERE idTestSetGangLeader = :id.';
+
+COMMENT ON INDEX TestSetsDoneCreatedBuildCatIdx IS
+ 'The TestSetsDoneCreatedBuildCatIdx is for testbox results, graph options and such.';
+
+COMMENT ON INDEX TestSetsGraphBoxIdx IS
+ 'For graphs.';
+
+COMMENT ON TYPE TestBoxState_T IS
+ 'TestBox state.
+
+@todo Consider drawing a state diagram for this.';
+
+COMMENT ON TABLE TestBoxStatuses IS
+ 'Testbox status table.
+
+History is not planned on this table.';
+
+COMMENT ON COLUMN TestBoxStatuses.tsUpdated IS
+ 'When this status was last updated.
+This is updated everytime the testbox talks to the test manager, thus it
+can easily be used to find testboxes which has stopped responding.
+
+This is used for timeout calculation during gang-gathering, so in that
+scenario it won''t be updated until the gang is gathered or we time out.';
+
+COMMENT ON COLUMN TestBoxStatuses.enmState IS
+ 'The current state.';
+
+COMMENT ON COLUMN TestBoxStatuses.iWorkItem IS
+ 'Interal work item number.
+This is used to pick and prioritize between multiple scheduling groups.';
+
+COMMENT ON TABLE GlobalResourceStatuses IS
+ 'Global resource status, tracks which test set resources are allocated by.
+
+History is not planned on this table.';
+
+COMMENT ON COLUMN GlobalResourceStatuses.tsAllocated IS
+ 'When the allocation took place.';
+
+COMMENT ON TABLE SchedQueues IS
+ 'Scheduler queue.
+
+The queues are currently associated with a scheduling group, it could
+alternative be changed to hook on to a testbox instead. It depends on what
+kind of scheduling method we prefer. The former method aims at test case
+thruput, making sacrifices in the hardware distribution area. The latter is
+more like the old buildbox style testing, making sure that each test case is
+executed on each testbox.
+
+When there are configuration changes, TM will regenerate the scheduling
+queue for the affected scheduling groups. We do not concern ourselves with
+trying to continue at the approximately same queue position, we simply take
+it from the top.
+
+When a testbox ask for work, we will open a cursor on the queue and take the
+first test in the queue that can be executed on that testbox. The test will
+be moved to the end of the queue (getting a new item_id).
+
+If a test is manually changed to the head of the queue, the item will get a
+item_id which is 1 lower than the head of the queue. Unless someone does
+this a couple of billion times, we shouldn''t have any trouble running out of
+number space. :-)
+
+Manually moving a test to the end of the queue is easy, just get a new
+''item_id''.
+
+History is not planned on this table.';
+
+COMMENT ON COLUMN SchedQueues.bmHourlySchedule IS
+ 'The scheduling time constraints (see SchedGroupMembers.bmHourlySchedule).';
+
+COMMENT ON COLUMN SchedQueues.tsConfig IS
+ 'When the queue entry was created and for which config is valid.
+This is the timestamp that should be used when reading config info.';
+
+COMMENT ON COLUMN SchedQueues.tsLastScheduled IS
+ 'When this status was last scheduled.
+This is set to current_timestamp when moving the entry to the end of the
+queue. It''s initial value is unix-epoch. Not entirely sure if it''s
+useful beyond introspection and non-unique foreign key hacking.';
+
+COMMENT ON COLUMN SchedQueues.cMissingGangMembers IS
+ 'The number of gang members still missing.
+
+This saves calculating the number of missing members via selects like:
+ SELECT COUNT(*) FROM TestSets WHERE idTestSetGangLeader = :idGang;
+and
+ SELECT cGangMembers FROM TestCaseArgs WHERE idGenTestCaseArgs = :idTest;
+to figure out whether to remain in ''gather-gang''::TestBoxState_T.';
+
+COMMENT ON INDEX SchedQueuesItemIdx IS
+ 'The number of times this has been considered for scheduling.
+cConsidered SMALLINT DEFAULT 0 NOT NULL,';
+
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseDefaultUserAccounts.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseDefaultUserAccounts.pgsql
new file mode 100644
index 00000000..41198b4f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseDefaultUserAccounts.pgsql
@@ -0,0 +1,43 @@
+-- $Id: TestManagerDatabaseDefaultUserAccounts.pgsql $
+--- @file
+-- VBox Test Manager default user account records creation script.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+\set ON_ERROR_STOP 1
+\connect testmanager;
+
+-- Add record for user 'admin'
+INSERT INTO Users (sUsername, sEmail, sFullName, sLoginName)
+ VALUES ('root', 'admin@example.org', 'Administrator', 'admin');
+
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks.pgsql
new file mode 100644
index 00000000..1430169c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks.pgsql
@@ -0,0 +1,90 @@
+-- $Id: TestManagerDatabaseForeignKeyErHacks.pgsql $
+--- @file
+-- VBox Test Manager Database Addendum that adds non-unique foreign keys.
+--
+-- This is for getting better visualization in reverse engeering ER tools,
+-- it is not for production databases.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+\set ON_ERROR_STOP 1
+\connect testmanager
+
+ALTER TABLE TestCaseArgs
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idTestCase, tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL;
+
+ALTER TABLE TestcaseDeps
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idTestCase, tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL;
+ALTER TABLE TestcaseDeps
+ ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idTestCasePreReq,tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL;
+
+ALTER TABLE TestCaseGlobalRsrcDeps
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idTestCase, tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL;
+ALTER TABLE TestCaseGlobalRsrcDeps
+ ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idGlobalRsrc, tsExpire) REFERENCES GlobalResources(idGlobalRsrc, tsExpire) MATCH FULL;
+
+ALTER TABLE TestGroupMembers
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idTestGroup, tsExpire) REFERENCES TestGroups(idTestGroup, tsExpire) MATCH FULL;
+ALTER TABLE TestGroupMembers
+ ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idTestCase, tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL;
+
+ALTER TABLE SchedGroups
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idBuildSrc, tsExpire) REFERENCES BuildSources(idBuildSrc, tsExpire) MATCH SIMPLE;
+ALTER TABLE SchedGroups
+ ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idBuildSrcTestSuite, tsExpire) REFERENCES BuildSources(idBuildSrc, tsExpire) MATCH SIMPLE;
+
+ALTER TABLE SchedGroupMembers
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idSchedGroup, tsExpire) REFERENCES SchedGroups(idSchedGroup, tsExpire) MATCH FULL;
+ALTER TABLE SchedGroupMembers
+ ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idTestGroup, tsExpire) REFERENCES TestGroups(idTestGroup, tsExpire) MATCH FULL;
+ALTER TABLE SchedGroupMembers
+ ADD CONSTRAINT non_unique_fk3 FOREIGN KEY (idTestGroupPreReq, tsExpire) REFERENCES TestGroups(idTestGroup, tsExpire) MATCH FULL;
+
+ALTER TABLE TestBoxes
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idSchedGroup, tsExpire) REFERENCES SchedGroups(idSchedGroup, tsExpire) MATCH FULL;
+
+ALTER TABLE FailureReasons
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idFailureCategory, tsExpire) REFERENCES FailureCategories(idFailureCategory, tsExpire) MATCH FULL;
+
+ALTER TABLE TestResultFailures
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idFailureReason, tsExpire) REFERENCES FailureReasons(idFailureReason, tsExpire) MATCH FULL;
+
+ALTER TABLE BuildBlacklist
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idFailureReason, tsExpire) REFERENCES FailureReasons(idFailureReason, tsExpire) MATCH FULL;
+
+ALTER TABLE GlobalResourceStatuses
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idGlobalRsrc, tsAllocated) REFERENCES GlobalResources(idGlobalRsrc, tsExpire) MATCH FULL;
+
+ALTER TABLE SchedQueues
+ ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idSchedGroup, tsLastScheduled) REFERENCES SchedGroups(idSchedGroup, tsExpire) MATCH FULL;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks2.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks2.pgsql
new file mode 100644
index 00000000..12686d81
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks2.pgsql
@@ -0,0 +1,77 @@
+-- $Id: TestManagerDatabaseForeignKeyErHacks2.pgsql $
+--- @file
+-- VBox Test Manager Database Addendum that adds non-unique foreign keys to Users.
+--
+-- This is for getting better visualization in reverse engeering ER tools,
+-- it is not for production databases.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+\set ON_ERROR_STOP 1
+\connect testmanager
+
+ALTER TABLE GlobalResources
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE BuildSources
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE RequirementSets
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsCreated) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE TestCases
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE TestCaseArgs
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE TestcaseDeps
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE TestCaseGlobalRsrcDeps
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE TestGroups
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE TestGroupMembers
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE SchedGroups
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH SIMPLE;
+ALTER TABLE SchedGroupMembers
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE TestBoxes
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE FailureCategories
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE FailureReasons
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE TestResultFailures
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE BuildBlacklist
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL;
+ALTER TABLE Builds
+ ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsCreated) REFERENCES Users(uid, tsExpire) MATCH FULL;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseInit.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseInit.pgsql
new file mode 100644
index 00000000..10e93ff2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseInit.pgsql
@@ -0,0 +1,1950 @@
+-- $Id: TestManagerDatabaseInit.pgsql $
+--- @file
+-- VBox Test Manager Database Creation script.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Declaimer:
+--
+-- The guys working on this design are not database experts, web
+-- programming experts or similar, rather we are low level guys
+-- who's main job is x86 & AMD64 virtualization. So, please don't
+-- be too hard on us. :-)
+--
+--
+
+
+-- D R O P D A T A B A S E t e s t m a n a g e r - - you do this now.
+\set ON_ERROR_STOP 1
+CREATE DATABASE testmanager;
+\connect testmanager;
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+--
+-- S y s t e m
+--
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+---
+-- Log table for a few important events.
+--
+-- Currently, two events are planned to be logged:
+-- - Sign on of an unknown testbox, including the IP and System UUID.
+-- This will be restricted to one entry per 24h or something like that:
+-- SELECT COUNT(*)
+-- FROM SystemLog
+-- WHERE tsCreated >= (current_timestamp - interval '24 hours')
+-- AND sEvent = 'TBoxUnkn'
+-- AND sLogText = :sNewLogText;
+-- - When cleaning up an abandoned testcase (scenario #9), log which
+-- testbox abandoned which testset.
+--
+-- The Web UI will have some way of displaying the log.
+--
+-- A batch job should regularly clean out old log messages, like for instance
+-- > 64 days.
+--
+CREATE TABLE SystemLog (
+ --- When this was logged.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The event type.
+ -- This is a 8 character string identifier so that we don't need to change
+ -- some enum type everytime we introduce a new event type.
+ sEvent CHAR(8) NOT NULL,
+ --- The log text.
+ sLogText text NOT NULL,
+
+ PRIMARY KEY (tsCreated, sEvent)
+);
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+--
+-- C o n f i g u r a t i o n
+--
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+--- @table Users
+-- Test manager users.
+--
+-- This is mainly for doing simple access checks before permitting access to
+-- the test manager. This needs to be coordinated with
+-- apache/ldap/Oracle-Single-Sign-On.
+--
+-- The main purpose, though, is for tracing who changed the test config and
+-- analysis data.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp.
+--
+CREATE SEQUENCE UserIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE Users (
+ --- The user id.
+ uid INTEGER DEFAULT NEXTVAL('UserIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER DEFAULT NULL,
+ --- User name.
+ sUsername text NOT NULL,
+ --- The email address of the user.
+ sEmail text NOT NULL,
+ --- The full name.
+ sFullName text NOT NULL,
+ --- The login name used by apache.
+ sLoginName text NOT NULL,
+ --- Read access only.
+ fReadOnly BOOLEAN NOT NULL DEFAULT FALSE,
+
+ PRIMARY KEY (uid, tsExpire)
+);
+CREATE INDEX UsersLoginNameIdx ON Users (sLoginName, tsExpire DESC);
+
+
+--- @table GlobalResources
+-- Global resource configuration.
+--
+-- For example an iSCSI target.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp.
+--
+CREATE SEQUENCE GlobalResourceIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE GlobalResources (
+ --- The global resource ID.
+ -- This stays the same thru updates.
+ idGlobalRsrc INTEGER DEFAULT NEXTVAL('GlobalResourceIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+ --- The name of the resource.
+ sName text NOT NULL,
+ --- Optional resource description.
+ sDescription text,
+ --- Indicates whether this resource is currently enabled (online).
+ fEnabled boolean DEFAULT FALSE NOT NULL,
+
+ PRIMARY KEY (idGlobalRsrc, tsExpire)
+);
+
+
+--- @table BuildSources
+-- Build sources.
+--
+-- This is used by a scheduling group to select builds and the default
+-- Validation Kit from the Builds table.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp.
+--
+-- @todo Any better way of representing this so we could more easily
+-- join/whatever when searching for builds?
+--
+CREATE SEQUENCE BuildSourceIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE BuildSources (
+ --- The build source identifier.
+ -- This stays constant over time.
+ idBuildSrc INTEGER DEFAULT NEXTVAL('BuildSourceIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ --- The name of the build source.
+ sName TEXT NOT NULL,
+ --- Description.
+ sDescription TEXT DEFAULT NULL,
+
+ --- Which product.
+ -- ASSUME that it is okay to limit a build source to a single product.
+ sProduct text NOT NULL,
+ --- Which branch.
+ -- ASSUME that it is okay to limit a build source to a branch.
+ sBranch text NOT NULL,
+
+ --- Build types to include, all matches if NULL.
+ -- @todo Weighting the types would be nice in a later version.
+ asTypes text ARRAY DEFAULT NULL,
+ --- Array of the 'sOs.sCpuArch' to match, all matches if NULL.
+ -- See KBUILD_OSES in kBuild for a list of standard target OSes, and
+ -- KBUILD_ARCHES for a list of standard architectures.
+ --
+ -- @remarks See marks on 'os-agnostic' and 'noarch' in BuildCategories.
+ asOsArches text ARRAY DEFAULT NULL,
+
+ --- The first subversion tree revision to match, no lower limit if NULL.
+ iFirstRevision INTEGER DEFAULT NULL,
+ --- The last subversion tree revision to match, no upper limit if NULL.
+ iLastRevision INTEGER DEFAULT NULL,
+
+ --- The maximum age of the builds in seconds, unlimited if NULL.
+ cSecMaxAge INTEGER DEFAULT NULL,
+
+ PRIMARY KEY (idBuildSrc, tsExpire)
+);
+
+
+--- @table TestCases
+-- Test case configuration.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp.
+--
+CREATE SEQUENCE TestCaseIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE SEQUENCE TestCaseGenIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestCases (
+ --- The fixed test case ID.
+ -- This is assigned when the test case is created and will never change.
+ idTestCase INTEGER DEFAULT NEXTVAL('TestCaseIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+ --- Generation ID for this row, a truly unique identifier.
+ -- This is primarily for referencing by TestSets.
+ idGenTestCase INTEGER UNIQUE DEFAULT NEXTVAL('TestCaseGenIdSeq') NOT NULL,
+
+ --- The name of the test case.
+ sName TEXT NOT NULL,
+ --- Optional test case description.
+ sDescription TEXT DEFAULT NULL,
+ --- Indicates whether this test case is currently enabled.
+ fEnabled BOOLEAN DEFAULT FALSE NOT NULL,
+ --- Default test case timeout given in seconds.
+ cSecTimeout INTEGER NOT NULL CHECK (cSecTimeout > 0),
+ --- Default TestBox requirement expression (python boolean expression).
+ -- All the scheduler properties are available for use with the same names
+ -- as in that table.
+ -- If NULL everything matches.
+ sTestBoxReqExpr TEXT DEFAULT NULL,
+ --- Default build requirement expression (python boolean expression).
+ -- The following build properties are available: sProduct, sBranch,
+ -- sType, asOsArches, sVersion, iRevision, uidAuthor and idBuild.
+ -- If NULL everything matches.
+ sBuildReqExpr TEXT DEFAULT NULL,
+
+ --- The base command.
+ -- String suitable for executing in bourne shell with space as separator
+ -- (IFS). References to @BUILD_BINARIES@ will be replaced WITH the content
+ -- of the Builds(sBinaries) field.
+ sBaseCmd TEXT NOT NULL,
+
+ --- Comma separated list of test suite zips (or tars) that the testbox will
+ -- need to download and expand prior to testing.
+ -- If NULL the current test suite of the scheduling group will be used (the
+ -- scheduling group will have an optional test suite build queue associated
+ -- with it). The current test suite can also be referenced by
+ -- @VALIDATIONKIT_ZIP@ in case more downloads are required. Files may also be
+ -- uploaded to the test manager download area, in which case the
+ -- @DOWNLOAD_BASE_URL@ prefix can be used to refer to this area.
+ sTestSuiteZips TEXT DEFAULT NULL,
+
+ -- Comment regarding a change or something.
+ sComment TEXT DEFAULT NULL,
+
+ PRIMARY KEY (idTestCase, tsExpire)
+);
+
+
+--- @table TestCaseArgs
+-- Test case argument list variations.
+--
+-- For example, we have a test case that does a set of tests on a virtual
+-- machine. To get better code/feature coverage of this testcase we wish to
+-- run it with different guest hardware configuration. The test case may do
+-- the same stuff, but the guest OS as well as the VMM may react differently to
+-- the hardware configurations and uncover issues in the VMM, device emulation
+-- or other places.
+--
+-- Typical hardware variations are:
+-- - guest memory size (RAM),
+-- - guest video memory size (VRAM),
+-- - virtual CPUs / cores / threads,
+-- - virtual chipset
+-- - virtual network interface card (NIC)
+-- - USB 1.1, USB 2.0, no USB
+--
+-- The TM web UI will help the user create a reasonable set of permutations
+-- of these parameters, the user specifies a maximum and the TM uses certain
+-- rules together with random selection to generate the desired number. The
+-- UI will also help suggest fitting testbox requirements according to the
+-- RAM/VRAM sizes and the virtual CPU counts. The user may then make
+-- adjustments to the suggestions before commit them.
+--
+-- Alternatively, the user may also enter all the permutations without any
+-- help from the UI.
+--
+-- Note! All test cases has at least one entry in this table, even if it is
+-- empty, because testbox requirements are specified thru this.
+--
+-- Querying the valid parameter lists for a testase this way:
+-- SELECT * ... WHERE idTestCase = TestCases.idTestCase
+-- AND tsExpire > <when>
+-- AND tsEffective <= <when>;
+--
+-- Querying the valid parameter list for the latest generation can be
+-- simplified by just checking tsExpire date:
+-- SELECT * ... WHERE idTestCase = TestCases.idTestCase
+-- AND tsExpire == TIMESTAMP WITH TIME ZONE 'infinity';
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp.
+--
+CREATE SEQUENCE TestCaseArgsIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE SEQUENCE TestCaseArgsGenIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestCaseArgs (
+ --- The test case ID.
+ -- Non-unique foreign key: TestCases(idTestCase).
+ idTestCase INTEGER NOT NULL,
+ --- The testcase argument variation ID (fixed).
+ -- This is primarily for TestGroupMembers.aidTestCaseArgs.
+ idTestCaseArgs INTEGER DEFAULT NEXTVAL('TestCaseArgsIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+ --- Generation ID for this row.
+ -- This is primarily for efficient referencing by TestSets and SchedQueues.
+ idGenTestCaseArgs INTEGER UNIQUE DEFAULT NEXTVAL('TestCaseArgsGenIdSeq') NOT NULL,
+
+ --- The additional arguments.
+ -- String suitable for bourne shell style argument parsing with space as
+ -- separator (IFS). References to @BUILD_BINARIES@ will be replaced with
+ -- the content of the Builds(sBinaries) field.
+ sArgs TEXT NOT NULL,
+ --- Optional test case timeout given in seconds.
+ -- If NULL, the TestCases.cSecTimeout field is used instead.
+ cSecTimeout INTEGER DEFAULT NULL CHECK (cSecTimeout IS NULL OR cSecTimeout > 0),
+ --- Additional TestBox requirement expression (python boolean expression).
+ -- All the scheduler properties are available for use with the same names
+ -- as in that table. This is checked after first checking the requirements
+ -- in the TestCases.sTestBoxReqExpr field.
+ sTestBoxReqExpr TEXT DEFAULT NULL,
+ --- Additional build requirement expression (python boolean expression).
+ -- The following build properties are available: sProduct, sBranch,
+ -- sType, asOsArches, sVersion, iRevision, uidAuthor and idBuild. This is
+ -- checked after first checking the requirements in the
+ -- TestCases.sBuildReqExpr field.
+ sBuildReqExpr TEXT DEFAULT NULL,
+ --- Number of testboxes required (gang scheduling).
+ cGangMembers SMALLINT DEFAULT 1 NOT NULL CHECK (cGangMembers > 0 AND cGangMembers < 1024),
+ --- Optional variation sub-name.
+ sSubName TEXT DEFAULT NULL,
+
+ --- The arguments are part of the primary key for several reasons.
+ -- No duplicate argument lists (makes no sense - if you want to prioritize
+ -- argument lists, we add that explicitly). This may hopefully enable us
+ -- to more easily check coverage later on, even when the test case is
+ -- reconfigured with more/less permutations.
+ PRIMARY KEY (idTestCase, tsExpire, sArgs)
+);
+CREATE INDEX TestCaseArgsLookupIdx ON TestCaseArgs (idTestCase, tsExpire DESC, tsEffective ASC);
+
+
+--- @table TestCaseDeps
+-- Test case dependencies (N:M)
+--
+-- This effect build selection. The build must have passed all runs of the
+-- given prerequisite testcase (idTestCasePreReq) and executed at a minimum one
+-- argument list variation.
+--
+-- This should also affect scheduling order, if possible at least one
+-- prerequisite testcase variation should be place before the specific testcase
+-- in the scheduling queue.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE TABLE TestCaseDeps (
+ --- The test case that depends on someone.
+ -- Non-unique foreign key: TestCases(idTestCase).
+ idTestCase INTEGER NOT NULL,
+ --- The prerequisite test case ID.
+ -- Non-unique foreign key: TestCases(idTestCase).
+ idTestCasePreReq INTEGER NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ PRIMARY KEY (idTestCase, idTestCasePreReq, tsExpire)
+);
+
+
+--- @table TestCaseGlobalRsrcDeps
+-- Test case dependencies on global resources (N:M)
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE TABLE TestCaseGlobalRsrcDeps (
+ --- The test case that depends on someone.
+ -- Non-unique foreign key: TestCases(idTestCase).
+ idTestCase INTEGER NOT NULL,
+ --- The prerequisite resource ID.
+ -- Non-unique foreign key: GlobalResources(idGlobalRsrc).
+ idGlobalRsrc INTEGER NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ PRIMARY KEY (idTestCase, idGlobalRsrc, tsExpire)
+);
+
+
+--- @table TestGroups
+-- Test Group - A collection of test cases.
+--
+-- This is for simplifying test configuration by working with a few groups
+-- instead of a herd of individual testcases. It may also be used for creating
+-- test suites for certain areas (like guest additions) or tasks (like
+-- performance measurements).
+--
+-- A test case can be member of any number of test groups.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE SEQUENCE TestGroupIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestGroups (
+ --- The fixed scheduling group ID.
+ -- This is assigned when the group is created and will never change.
+ idTestGroup INTEGER DEFAULT NEXTVAL('TestGroupIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ --- The name of the scheduling group.
+ sName TEXT NOT NULL,
+ --- Optional group description.
+ sDescription TEXT,
+ -- Comment regarding a change or something.
+ sComment TEXT DEFAULT NULL,
+
+ PRIMARY KEY (idTestGroup, tsExpire)
+);
+CREATE INDEX TestGroups_id_index ON TestGroups (idTestGroup, tsExpire DESC, tsEffective ASC);
+
+
+--- @table TestGroupMembers
+-- The N:M relationship between test case configurations and test groups.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE TABLE TestGroupMembers (
+ --- The group ID.
+ -- Non-unique foreign key: TestGroups(idTestGroup).
+ idTestGroup INTEGER NOT NULL,
+ --- The test case ID.
+ -- Non-unique foreign key: TestCases(idTestCase).
+ idTestCase INTEGER NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ --- Test case scheduling priority.
+ -- Higher number causes the test case to be run more frequently.
+ -- @sa SchedGroupMembers.iSchedPriority, TestBoxesInSchedGroups.iSchedPriority
+ -- @todo Not sure we want to keep this...
+ iSchedPriority INTEGER DEFAULT 16 CHECK (iSchedPriority >= 0 AND iSchedPriority < 32) NOT NULL,
+
+ --- Limit the memberships to the given argument variations.
+ -- Non-unique foreign key: TestCaseArgs(idTestCase, idTestCaseArgs).
+ aidTestCaseArgs INTEGER ARRAY DEFAULT NULL,
+
+ PRIMARY KEY (idTestGroup, idTestCase, tsExpire)
+);
+
+
+--- @table SchedGroups
+-- Scheduling group (aka. testbox partitioning) configuration.
+--
+-- A testbox is associated with exactly one scheduling group. This association
+-- can be changed, of course. If we (want to) retire a group which still has
+-- testboxes associated with it, these will be moved to the 'default' group.
+--
+-- The TM web UI will make sure that a testbox is always in a group and that
+-- the default group cannot be deleted.
+--
+-- A scheduling group combines several things:
+-- - A selection of builds to test (via idBuildSrc).
+-- - A collection of test groups to test with (via SchedGroupMembers).
+-- - A set of testboxes to test on (via TestBoxes.idSchedGroup).
+--
+-- In additions there is an optional source of fresh test suite builds (think
+-- VBoxTestSuite) as well as scheduling options.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE TYPE Scheduler_T AS ENUM (
+ 'bestEffortContinousItegration',
+ 'reserved'
+);
+CREATE SEQUENCE SchedGroupIdSeq
+ START 2
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE SchedGroups (
+ --- The fixed scheduling group ID.
+ -- This is assigned when the group is created and will never change.
+ idSchedGroup INTEGER DEFAULT NEXTVAL('SchedGroupIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ -- @note This is NULL for the default group.
+ uidAuthor INTEGER DEFAULT NULL,
+
+ --- The name of the scheduling group.
+ sName TEXT NOT NULL,
+ --- Optional group description.
+ sDescription TEXT,
+ --- Indicates whether this group is currently enabled.
+ fEnabled boolean NOT NULL,
+ --- The scheduler to use.
+ -- This is for when we later desire different scheduling that the best
+ -- effort stuff provided by the initial implementation.
+ enmScheduler Scheduler_T DEFAULT 'bestEffortContinousItegration'::Scheduler_T NOT NULL,
+ --- The build source.
+ -- Non-unique foreign key: BuildSources(idBuildSrc)
+ idBuildSrc INTEGER DEFAULT NULL,
+ --- The Validation Kit build source (@VALIDATIONKIT_ZIP@).
+ -- Non-unique foreign key: BuildSources(idBuildSrc)
+ idBuildSrcTestSuite INTEGER DEFAULT NULL,
+ -- Comment regarding a change or something.
+ sComment TEXT DEFAULT NULL,
+
+ PRIMARY KEY (idSchedGroup, tsExpire)
+);
+
+-- Special default group.
+INSERT INTO SchedGroups (idSchedGroup, tsEffective, tsExpire, sName, sDescription, fEnabled)
+ VALUES (1, TIMESTAMP WITH TIME ZONE 'epoch', TIMESTAMP WITH TIME ZONE 'infinity', 'default', 'default group', FALSE);
+
+
+--- @table SchedGroupMembers
+-- N:M relationship between scheduling groups and test groups.
+--
+-- Several scheduling parameters are associated with this relationship.
+--
+-- The test group dependency (idTestGroupPreReq) can be used in the same way as
+-- TestCaseDeps.idTestCasePreReq, only here on test group level. This means it
+-- affects the build selection. The builds needs to have passed all test runs
+-- the prerequisite test group and done at least one argument variation of each
+-- test case in it.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE TABLE SchedGroupMembers (
+ --- Scheduling ID.
+ -- Non-unique foreign key: SchedGroups(idSchedGroup).
+ idSchedGroup INTEGER NOT NULL,
+ --- Testgroup ID.
+ -- Non-unique foreign key: TestGroups(idTestGroup).
+ idTestGroup INTEGER NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ --- The scheduling priority of the test group.
+ -- Higher number causes the test case to be run more frequently.
+ -- @sa TestGroupMembers.iSchedPriority, TestBoxesInSchedGroups.iSchedPriority
+ iSchedPriority INTEGER DEFAULT 16 CHECK (iSchedPriority >= 0 AND iSchedPriority < 32) NOT NULL,
+ --- When during the week this group is allowed to start running, NULL means
+ -- there are no constraints.
+ -- Each bit in the bitstring represents one hour, with bit 0 indicating the
+ -- midnight hour on a monday.
+ bmHourlySchedule bit(168) DEFAULT NULL,
+ --- Optional test group dependency.
+ -- Non-unique foreign key: TestGroups(idTestGroup).
+ -- This is for requiring that a build has been subject to smoke tests
+ -- before bothering to subject it to longer tests.
+ -- @todo Not entirely sure this should be here, but I'm not so keen on yet
+ -- another table as the only use case is smoketests.
+ idTestGroupPreReq INTEGER DEFAULT NULL,
+
+ PRIMARY KEY (idSchedGroup, idTestGroup, tsExpire)
+);
+
+
+--- @table TestBoxStrTab
+-- String table for the test boxes.
+--
+-- This is a string cache for all string members in TestBoxes except the name.
+-- The rational is to avoid duplicating large strings like sReport when the
+-- testbox reports a new cMbScratch value or the box when the test sheriff
+-- sends a reboot command or similar.
+--
+-- At the time this table was introduced, we had 400558 TestBoxes rows, where
+-- the SUM(LENGTH(sReport)) was 993MB. There were really just 1066 distinct
+-- sReport values, with a total length of 0x3 MB.
+--
+-- Nothing is ever deleted from this table.
+--
+-- @note Should use a stored procedure to query/insert a string.
+--
+--
+-- TestBox stats prior to conversion:
+-- SELECT COUNT(*) FROM TestBoxes: 400558 rows
+-- SELECT pg_total_relation_size('TestBoxes'): 740794368 bytes (706 MB)
+-- Average row cost: 740794368 / 400558 = 1849 bytes/row
+--
+-- After conversion:
+-- SELECT COUNT(*) FROM TestBoxes: 400558 rows
+-- SELECT pg_total_relation_size('TestBoxes'): 144375808 bytes (138 MB)
+-- SELECT COUNT(idStr) FROM TestBoxStrTab: 1292 rows
+-- SELECT pg_total_relation_size('TestBoxStrTab'): 5709824 bytes (5.5 MB)
+-- (144375808 + 5709824) / 740794368 = 20 %
+-- Average row cost boxes: 144375808 / 400558 = 360 bytes/row
+-- Average row cost strings: 5709824 / 1292 = 4420 bytes/row
+--
+CREATE SEQUENCE TestBoxStrTabIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestBoxStrTab (
+ --- The ID of this string.
+ idStr INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestBoxStrTabIdSeq'),
+ --- The string value.
+ sValue text NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL
+);
+-- Note! Must use hash index as the sReport strings are too long for regular indexing.
+CREATE INDEX TestBoxStrTabNameIdx ON TestBoxStrTab USING hash (sValue);
+
+--- Empty string with ID 0.
+INSERT INTO TestBoxStrTab (idStr, sValue) VALUES (0, '');
+
+
+--- @type TestBoxCmd_T
+-- Testbox commands.
+CREATE TYPE TestBoxCmd_T AS ENUM (
+ 'none',
+ 'abort',
+ 'reboot', --< This implies abort. Status changes when reaching 'idle'.
+ 'upgrade', --< This is only handled when asking for work.
+ 'upgrade-and-reboot', --< Ditto.
+ 'special' --< Similar to upgrade, reserved for the future.
+);
+
+
+--- @type LomKind_T
+-- The kind of lights out management on a testbox.
+CREATE TYPE LomKind_T AS ENUM (
+ 'none',
+ 'ilom',
+ 'elom',
+ 'apple-xserve-lom'
+);
+
+
+--- @table TestBoxes
+-- Testbox configurations.
+--
+-- The testboxes are identified by IP and the system UUID if available. Should
+-- the IP change, the testbox will be refused at sign on and the testbox
+-- sheriff will have to update it's IP.
+--
+-- @todo Implement the UUID stuff. Get it from DMI, UEFI or whereever.
+-- Mismatching needs to be logged somewhere...
+--
+-- To query the currently valid configuration:
+-- SELECT ... WHERE id = idTestBox AND tsExpire = TIMESTAMP WITH TIME ZONE 'infinity';
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE SEQUENCE TestBoxIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE SEQUENCE TestBoxGenIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestBoxes (
+ --- The fixed testbox ID.
+ -- This is assigned when the testbox is created and will never change.
+ idTestBox INTEGER DEFAULT NEXTVAL('TestBoxIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- When modified automatically by the testbox, NULL is used.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER DEFAULT NULL,
+ --- Generation ID for this row.
+ -- This is primarily for referencing by TestSets.
+ idGenTestBox INTEGER UNIQUE DEFAULT NEXTVAL('TestBoxGenIdSeq') NOT NULL,
+
+ --- The testbox IP.
+ -- This is from the webserver point of view and automatically updated on
+ -- SIGNON. The test setup doesn't permit for IP addresses to change while
+ -- the testbox is operational, because this will break gang tests.
+ ip inet NOT NULL,
+ --- The system or firmware UUID.
+ -- This uniquely identifies the testbox when talking to the server. After
+ -- SIGNON though, the testbox will also provide idTestBox and ip to
+ -- establish its identity beyond doubt.
+ uuidSystem uuid NOT NULL,
+ --- The testbox name.
+ -- Usually similar to the DNS name.
+ sName text NOT NULL,
+ --- Optional testbox description.
+ -- Intended for describing the box as well as making other relevant notes.
+ idStrDescription INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+
+ --- Indicates whether this testbox is enabled.
+ -- A testbox gets disabled when we're doing maintenance, debugging a issue
+ -- that happens only on that testbox, or some similar stuff. This is an
+ -- alternative to deleting the testbox.
+ fEnabled BOOLEAN DEFAULT NULL,
+
+ --- The kind of lights-out-management.
+ enmLomKind LomKind_T DEFAULT 'none'::LomKind_T NOT NULL,
+ --- The IP adress of the lights-out-management.
+ -- This can be NULL if enmLomKind is 'none', otherwise it must contain a valid address.
+ ipLom inet DEFAULT NULL,
+
+ --- Timeout scale factor, given as a percent.
+ -- This is a crude adjustment of the test case timeout for slower hardware.
+ pctScaleTimeout smallint DEFAULT 100 NOT NULL CHECK (pctScaleTimeout > 10 AND pctScaleTimeout < 20000),
+
+ --- Change comment or similar.
+ idStrComment INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+
+ --- @name Scheduling properties (reported by testbox script).
+ -- @{
+ --- Same abbrieviations as kBuild, see KBUILD_OSES.
+ idStrOs INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- Informational, no fixed format.
+ idStrOsVersion INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- Same as CPUID reports (GenuineIntel, AuthenticAMD, CentaurHauls, ...).
+ idStrCpuVendor INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- Same as kBuild - x86, amd64, ... See KBUILD_ARCHES.
+ idStrCpuArch INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- The CPU name if available.
+ idStrCpuName INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- Number identifying the CPU family/model/stepping/whatever.
+ -- For x86 and AMD64 type CPUs, this will on the following format:
+ -- (EffFamily << 24) | (EffModel << 8) | Stepping.
+ lCpuRevision bigint DEFAULT NULL,
+ --- Number of CPUs, CPU cores and CPU threads.
+ cCpus smallint DEFAULT NULL CHECK (cCpus IS NULL OR cCpus > 0),
+ --- Set if capable of hardware virtualization.
+ fCpuHwVirt boolean DEFAULT NULL,
+ --- Set if capable of nested paging.
+ fCpuNestedPaging boolean DEFAULT NULL,
+ --- Set if CPU capable of 64-bit (VBox) guests.
+ fCpu64BitGuest boolean DEFAULT NULL,
+ --- Set if chipset with usable IOMMU (VT-d / AMD-Vi).
+ fChipsetIoMmu boolean DEFAULT NULL,
+ --- Set if the test box does raw-mode tests.
+ fRawMode boolean DEFAULT NULL,
+ --- The (approximate) memory size in megabytes (rounded down to nearest 4 MB).
+ cMbMemory bigint DEFAULT NULL CHECK (cMbMemory IS NULL OR cMbMemory > 0),
+ --- The amount of scratch space in megabytes (rounded down to nearest 64 MB).
+ cMbScratch bigint DEFAULT NULL CHECK (cMbScratch IS NULL OR cMbScratch >= 0),
+ --- Free form hardware and software report field.
+ idStrReport INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- @}
+
+ --- The testbox script revision number, serves the purpose of a version number.
+ -- Probably good to have when scheduling upgrades as well for status purposes.
+ iTestBoxScriptRev INTEGER DEFAULT 0 NOT NULL,
+ --- The python sys.hexversion (layed out as of 2.7).
+ -- Good to know which python versions we need to support.
+ iPythonHexVersion INTEGER DEFAULT NULL,
+
+ --- Pending command.
+ -- @note We put it here instead of in TestBoxStatuses to get history.
+ enmPendingCmd TestBoxCmd_T DEFAULT 'none'::TestBoxCmd_T NOT NULL,
+
+ PRIMARY KEY (idTestBox, tsExpire),
+
+ --- Nested paging requires hardware virtualization.
+ CHECK (fCpuNestedPaging IS NULL OR (fCpuNestedPaging <> TRUE OR fCpuHwVirt = TRUE))
+);
+CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire DESC);
+CREATE INDEX TestBoxesExpireEffectiveIdx ON TestBoxes (tsExpire DESC, tsEffective ASC);
+
+
+--
+-- Create a view for TestBoxes where the strings are resolved.
+--
+CREATE VIEW TestBoxesWithStrings AS
+ SELECT TestBoxes.*,
+ Str1.sValue AS sDescription,
+ Str2.sValue AS sComment,
+ Str3.sValue AS sOs,
+ Str4.sValue AS sOsVersion,
+ Str5.sValue AS sCpuVendor,
+ Str6.sValue AS sCpuArch,
+ Str7.sValue AS sCpuName,
+ Str8.sValue AS sReport
+ FROM TestBoxes
+ LEFT OUTER JOIN TestBoxStrTab Str1 ON idStrDescription = Str1.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str2 ON idStrComment = Str2.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str3 ON idStrOs = Str3.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str4 ON idStrOsVersion = Str4.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str5 ON idStrCpuVendor = Str5.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str6 ON idStrCpuArch = Str6.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str7 ON idStrCpuName = Str7.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str8 ON idStrReport = Str8.idStr;
+
+
+--- @table TestBoxesInSchedGroups
+-- N:M relationship between test boxes and scheduling groups.
+--
+-- We associate a priority with this relationship.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE TABLE TestBoxesInSchedGroups (
+ --- TestBox ID.
+ -- Non-unique foreign key: TestBoxes(idTestBox).
+ idTestBox INTEGER NOT NULL,
+ --- Scheduling ID.
+ -- Non-unique foreign key: SchedGroups(idSchedGroup).
+ idSchedGroup INTEGER NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ --- The scheduling priority of the scheduling group for the test box.
+ -- Higher number causes the scheduling group to be serviced more frequently.
+ -- @sa TestGroupMembers.iSchedPriority, SchedGroups.iSchedPriority
+ iSchedPriority INTEGER DEFAULT 16 CHECK (iSchedPriority >= 0 AND iSchedPriority < 32) NOT NULL,
+
+ PRIMARY KEY (idTestBox, idSchedGroup, tsExpire)
+);
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+--
+-- F a i l u r e T r a c k i n g
+--
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+
+--- @table FailureCategories
+-- Failure categories.
+--
+-- This is for organizing the failure reasons.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE SEQUENCE FailureCategoryIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE FailureCategories (
+ --- The identifier of this failure category (once assigned, it will never change).
+ idFailureCategory INTEGER DEFAULT NEXTVAL('FailureCategoryIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+ --- The short category description.
+ -- For combo boxes and other selection lists.
+ sShort text NOT NULL,
+ --- Full description
+ -- For cursor-over-poppups for instance.
+ sFull text NOT NULL,
+
+ PRIMARY KEY (idFailureCategory, tsExpire)
+);
+
+
+--- @table FailureReasons
+-- Failure reasons.
+--
+-- When analysing a test failure, the testbox sheriff will try assign a fitting
+-- reason for the failure. This table is here to help the sheriff in his/hers
+-- job as well as developers looking checking if their changes affected the
+-- test results in any way.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE SEQUENCE FailureReasonIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE FailureReasons (
+ --- The identifier of this failure reason (once assigned, it will never change).
+ idFailureReason INTEGER DEFAULT NEXTVAL('FailureReasonIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ --- The failure category this reason belongs to.
+ -- Non-unique foreign key: FailureCategories(idFailureCategory)
+ idFailureCategory INTEGER NOT NULL,
+ --- The short failure description.
+ -- For combo boxes and other selection lists.
+ sShort text NOT NULL,
+ --- Full failure description.
+ sFull text NOT NULL,
+ --- Ticket number in the primary bugtracker.
+ iTicket INTEGER DEFAULT NULL,
+ --- Other URLs to reports or discussions of the observed symptoms.
+ asUrls text ARRAY DEFAULT NULL,
+
+ PRIMARY KEY (idFailureReason, tsExpire)
+);
+CREATE INDEX FailureReasonsCategoryIdx ON FailureReasons (idFailureCategory, idFailureReason);
+
+
+
+--- @table TestResultFailures
+-- This is for tracking/discussing test result failures.
+--
+-- The rational for putting this is a separate table is that we need history on
+-- this while TestResults does not.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE TABLE TestResultFailures (
+ --- The test result we're disucssing.
+ -- @note The foreign key is declared after TestResults (further down).
+ idTestResult INTEGER NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+ --- The testsest this result is a part of.
+ -- This is mainly an aid for bypassing the enormous TestResults table.
+ -- Note! This is a foreign key, but we have to add it after TestSets has
+ -- been created, see further down.
+ idTestSet INTEGER NOT NULL,
+
+ --- The suggested failure reason.
+ -- Non-unique foreign key: FailureReasons(idFailureReason)
+ idFailureReason INTEGER NOT NULL,
+ --- Optional comment.
+ sComment text DEFAULT NULL,
+
+ PRIMARY KEY (idTestResult, tsExpire)
+);
+CREATE INDEX TestResultFailureIdx ON TestResultFailures (idTestSet, tsExpire DESC, tsEffective ASC);
+CREATE INDEX TestResultFailureIdx2 ON TestResultFailures (idTestResult, tsExpire DESC, tsEffective ASC);
+CREATE INDEX TestResultFailureIdx3 ON TestResultFailures (idFailureReason, idTestResult, tsExpire DESC, tsEffective ASC);
+
+
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+--
+-- T e s t I n p u t
+--
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+
+--- @table BuildBlacklist
+-- Table used to blacklist sets of builds.
+--
+-- The best usage example is a VMM developer realizing that a change causes the
+-- host to panic, hang, or otherwise misbehave. To prevent the testbox sheriff
+-- from repeatedly having to reboot testboxes, the builds gets blacklisted
+-- until there is a working build again. This may mean adding an open ended
+-- blacklist spec and then updating it with the final revision number once the
+-- fix has been committed.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+-- @todo Would be nice if we could replace the text strings below with a set of
+-- BuildCategories, or sore it in any other way which would enable us to
+-- do a negative join with build category... The way it is specified
+-- now, it looks like we have to open a cursor of prospecitve builds and
+-- filter then thru this table one by one.
+--
+-- Any better representation is welcome, but this is low prioirty for
+-- now, as it's relatively easy to change this later one.
+--
+CREATE SEQUENCE BuildBlacklistIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE BuildBlacklist (
+ --- The blacklist entry id.
+ -- This stays constant over time.
+ idBlacklisting INTEGER DEFAULT NEXTVAL('BuildBlacklistIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ --- The reason for the blacklisting.
+ -- Non-unique foreign key: FailureReasons(idFailureReason)
+ idFailureReason INTEGER NOT NULL,
+
+ --- Which product.
+ -- ASSUME that it is okay to limit a blacklisting to a single product.
+ sProduct text NOT NULL,
+ --- Which branch.
+ -- ASSUME that it is okay to limit a blacklisting to a branch.
+ sBranch text NOT NULL,
+
+ --- Build types to include, all matches if NULL.
+ asTypes text ARRAY DEFAULT NULL,
+ --- Array of the 'sOs.sCpuArch' to match, all matches if NULL.
+ -- See KBUILD_OSES in kBuild for a list of standard target OSes, and
+ -- KBUILD_ARCHES for a list of standard architectures.
+ --
+ -- @remarks See marks on 'os-agnostic' and 'noarch' in BuildCategories.
+ asOsArches text ARRAY DEFAULT NULL,
+
+ --- The first subversion tree revision to blacklist.
+ iFirstRevision INTEGER NOT NULL,
+ --- The last subversion tree revision to blacklist, no upper limit if NULL.
+ iLastRevision INTEGER NOT NULL,
+
+ PRIMARY KEY (idBlacklisting, tsExpire)
+);
+CREATE INDEX BuildBlacklistIdx ON BuildBlacklist (iLastRevision DESC, iFirstRevision ASC, sProduct, sBranch,
+ tsExpire DESC, tsEffective ASC);
+
+--- @table BuildCategories
+-- Build categories.
+--
+-- The purpose of this table is saving space in the Builds table and hopefully
+-- speed things up when selecting builds as well (compared to selecting on 4
+-- text fields in the much larger Builds table).
+--
+-- Insert only table, no update, no delete. History is not needed.
+--
+CREATE SEQUENCE BuildCategoryIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE BuildCategories (
+ --- The build type identifier.
+ idBuildCategory INTEGER PRIMARY KEY DEFAULT NEXTVAL('BuildCategoryIdSeq') NOT NULL,
+ --- Product.
+ -- The product name. For instance 'VBox' or 'VBoxTestSuite'.
+ sProduct TEXT NOT NULL,
+ --- The version control repository name.
+ sRepository TEXT NOT NULL,
+ --- The branch name (in the version control system).
+ sBranch TEXT NOT NULL,
+ --- The build type.
+ -- See KBUILD_BLD_TYPES in kBuild for a list of standard build types.
+ sType TEXT NOT NULL,
+ --- Array of the 'sOs.sCpuArch' supported by the build.
+ -- See KBUILD_OSES in kBuild for a list of standard target OSes, and
+ -- KBUILD_ARCHES for a list of standard architectures.
+ --
+ -- @remarks 'os-agnostic' is used if the build doesn't really target any
+ -- specific OS or if it targets all applicable OSes.
+ -- 'noarch' is used if the build is architecture independent or if
+ -- all applicable architectures are handled.
+ -- Thus, 'os-agnostic.noarch' will run on all build boxes.
+ --
+ -- @note The array shall be sorted ascendingly to prevent unnecessary duplicates!
+ --
+ asOsArches TEXT ARRAY NOT NULL,
+
+ UNIQUE (sProduct, sRepository, sBranch, sType, asOsArches)
+);
+
+
+--- @table Builds
+-- The builds table contains builds from the tinderboxes and oaccasionally from
+-- developers.
+--
+-- The tinderbox side could be fed by a batch job enumerating the build output
+-- directories every so often, looking for new builds. Or we could query them
+-- from the tinderbox database. Yet another alternative is making the
+-- tinderbox server or client side software inform us about all new builds.
+--
+-- The developer builds are entered manually thru the TM web UI. They are used
+-- for subjecting new code to some larger scale testing before commiting,
+-- enabling, or merging a private branch.
+--
+-- The builds are being selected from this table by the via the build source
+-- specification that SchedGroups.idBuildSrc and
+-- SchedGroups.idBuildSrcTestSuite links to.
+--
+-- @remarks This table stores history. Never update or delete anything. The
+-- equivalent of deleting is done by setting the 'tsExpire' field to
+-- current_timestamp. To select the currently valid entries use
+-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'.
+--
+CREATE SEQUENCE BuildIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE Builds (
+ --- The build identifier.
+ -- This remains unchanged
+ idBuild INTEGER DEFAULT NEXTVAL('BuildIdSeq') NOT NULL,
+ --- When this build was created or entered into the database.
+ -- This remains unchanged
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ -- @note This is NULL if added by a batch job / tinderbox.
+ uidAuthor INTEGER DEFAULT NULL,
+ --- The build category.
+ idBuildCategory INTEGER REFERENCES BuildCategories(idBuildCategory) NOT NULL,
+ --- The subversion tree revision of the build.
+ iRevision INTEGER NOT NULL,
+ --- The product version number (suitable for RTStrVersionCompare).
+ sVersion TEXT NOT NULL,
+ --- The link to the tinderbox log of this build.
+ sLogUrl TEXT,
+ --- Comma separated list of binaries.
+ -- The binaries have paths relative to the TESTBOX_PATH_BUILDS or full URLs.
+ sBinaries TEXT NOT NULL,
+ --- Set when the binaries gets deleted by the build quota script.
+ fBinariesDeleted BOOLEAN DEFAULT FALSE NOT NULL,
+
+ UNIQUE (idBuild, tsExpire)
+);
+CREATE INDEX BuildsLookupIdx ON Builds (idBuildCategory, iRevision);
+
+
+--- @table VcsRevisions
+-- This table is for translating build revisions into commit details.
+--
+-- For graphs and test results, it would be useful to translate revisions into
+-- dates and maybe provide commit message and the committer.
+--
+-- Data is entered exclusively thru one or more batch jobs, so no internal
+-- authorship needed. Also, since we're mirroring data from external sources
+-- here, the batch job is allowed to update/replace existing records.
+--
+-- @todo We we could collect more info from the version control systems, if we
+-- believe it's useful and can be presented in a reasonable manner.
+-- Getting a list of affected files would be simple (requires
+-- a separate table with a M:1 relationship to this table), or try
+-- associate a commit to a branch.
+--
+CREATE TABLE VcsRevisions (
+ --- The version control tree name.
+ sRepository TEXT NOT NULL,
+ --- The version control tree revision number.
+ iRevision INTEGER NOT NULL,
+ --- When the revision was created (committed).
+ tsCreated TIMESTAMP WITH TIME ZONE NOT NULL,
+ --- The name of the committer.
+ -- @note Not to be confused with uidAuthor and test manager users.
+ sAuthor TEXT,
+ --- The commit message.
+ sMessage TEXT,
+
+ UNIQUE (sRepository, iRevision)
+);
+CREATE INDEX VcsRevisionsByDate ON VcsRevisions (tsCreated DESC);
+
+
+--- @table VcsBugReferences
+-- This is for relating commits to a bug and vice versa.
+--
+-- This feature isn't so much for the test manager as a cheap way of extending
+-- bug trackers without VCS integration. We just need to parse the commit
+-- messages when inserting them into the VcsRevisions table.
+--
+-- Same input, updating and history considerations as VcsRevisions.
+--
+CREATE TABLE VcsBugReferences (
+ --- The version control tree name.
+ sRepository TEXT NOT NULL,
+ --- The version control tree revision number.
+ iRevision INTEGER NOT NULL,
+ --- The bug tracker identifier - see g_kdBugTrackers in config.py.
+ sBugTracker CHAR(4) NOT NULL,
+ --- The bug number in the bug tracker.
+ lBugNo BIGINT NOT NULL,
+
+ UNIQUE (sRepository, iRevision, sBugTracker, lBugNo)
+);
+CREATE INDEX VcsBugReferencesLookupIdx ON VcsBugReferences (sBugTracker, lBugNo);
+
+
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+--
+-- T e s t R e s u l t s
+--
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+
+--- @table TestResultStrTab
+-- String table for the test results.
+--
+-- This is a string cache for value names, test names and possible more, that
+-- is frequently repated in the test results record for each test run. The
+-- purpose is not only to save space, but to make datamining queries faster by
+-- giving them integer fields to work on instead of text fields. There may
+-- possibly be some benefits on INSERT as well as there are only integer
+-- indexes.
+--
+-- Nothing is ever deleted from this table.
+--
+-- @note Should use a stored procedure to query/insert a string.
+--
+CREATE SEQUENCE TestResultStrTabIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestResultStrTab (
+ --- The ID of this string.
+ idStr INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultStrTabIdSeq'),
+ --- The string value.
+ sValue text NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL
+);
+CREATE UNIQUE INDEX TestResultStrTabNameIdx ON TestResultStrTab (sValue);
+
+--- Empty string with ID 0.
+INSERT INTO TestResultStrTab (idStr, sValue) VALUES (0, '');
+
+
+--- @type TestStatus_T
+-- The status of a test (set / result).
+--
+CREATE TYPE TestStatus_T AS ENUM (
+ -- Initial status:
+ 'running',
+ -- Final statuses:
+ 'success',
+ -- Final status: Test didn't fail as such, it was something else.
+ 'skipped',
+ 'bad-testbox',
+ 'aborted',
+ -- Final status: Test failed.
+ 'failure',
+ 'timed-out',
+ 'rebooted'
+);
+
+
+--- @table TestResults
+-- Test results - a recursive bundle of joy!
+--
+-- A test case will be created when the testdriver calls reporter.testStart and
+-- concluded with reporter.testDone. The testdriver (or it subordinates) can
+-- use these methods to create nested test results. For IPRT based test cases,
+-- RTTestCreate, RTTestInitAndCreate and RTTestSub will both create new test
+-- result records, where as RTTestSubDone, RTTestSummaryAndDestroy and
+-- RTTestDestroy will conclude records.
+--
+-- By concluding is meant updating the status. When the test driver reports
+-- success, we check it against reported results. (paranoia strikes again!)
+--
+-- Nothing is ever deleted from this table.
+--
+-- @note As seen below, several other tables associate data with a
+-- test result, and the top most test result is referenced by the
+-- test set.
+--
+CREATE SEQUENCE TestResultIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestResults (
+ --- The ID of this test result.
+ idTestResult INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultIdSeq'),
+ --- The parent test result.
+ -- This is NULL for the top test result.
+ idTestResultParent INTEGER REFERENCES TestResults(idTestResult),
+ --- The test set this result is a part of.
+ -- Note! This is a foreign key, but we have to add it after TestSets has
+ -- been created, see further down.
+ idTestSet INTEGER NOT NULL,
+ --- Creation time stamp. This may also be the timestamp of when the test started.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The elapsed time for this test.
+ -- This is either reported by the directly (with some sanity checking) or
+ -- calculated (current_timestamp - created_ts).
+ -- @todo maybe use a nanosecond field here, check with what
+ tsElapsed interval DEFAULT NULL,
+ --- The test name.
+ idStrName INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL,
+ --- The error count.
+ cErrors INTEGER DEFAULT 0 NOT NULL,
+ --- The test status.
+ enmStatus TestStatus_T DEFAULT 'running'::TestStatus_T NOT NULL,
+ --- Nesting depth.
+ iNestingDepth smallint NOT NULL CHECK (iNestingDepth >= 0 AND iNestingDepth < 16),
+ -- Make sure errors and status match up.
+ CONSTRAINT CheckStatusMatchesErrors
+ CHECK ( (cErrors > 0 AND enmStatus IN ('running'::TestStatus_T,
+ 'failure'::TestStatus_T, 'timed-out'::TestStatus_T, 'rebooted'::TestStatus_T ))
+ OR (cErrors = 0 AND enmStatus IN ('running'::TestStatus_T, 'success'::TestStatus_T,
+ 'skipped'::TestStatus_T, 'aborted'::TestStatus_T, 'bad-testbox'::TestStatus_T))
+ ),
+ -- The following is for the TestResultFailures foreign key.
+ -- Note! This was added with the name TestResults_idTestResult_idTestSet_key in the tmdb-r16 update script.
+ UNIQUE (idTestResult, idTestSet)
+);
+
+CREATE INDEX TestResultsSetIdx ON TestResults (idTestSet, idStrName, idTestResult);
+CREATE INDEX TestResultsParentIdx ON TestResults (idTestResultParent);
+-- The TestResultsNameIdx and TestResultsNameIdx2 are for speeding up the result graph & reporting code.
+CREATE INDEX TestResultsNameIdx ON TestResults (idStrName, tsCreated DESC);
+CREATE INDEX TestResultsNameIdx2 ON TestResults (idTestResult, idStrName);
+
+ALTER TABLE TestResultFailures ADD CONSTRAINT TestResultFailures_idTestResult_idTestSet_fkey
+ FOREIGN KEY (idTestResult, idTestSet) REFERENCES TestResults(idTestResult, idTestSet) MATCH FULL;
+
+
+--- @table TestResultValues
+-- Test result values.
+--
+-- A testdriver or subordinate may report a test value via
+-- reporter.testValue(), while IPRT based test will use RTTestValue and
+-- associates.
+--
+-- This is an insert only table, no deletes, no updates.
+--
+CREATE SEQUENCE TestResultValueIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestResultValues (
+ --- The ID of this value.
+ idTestResultValue INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultValueIdSeq'),
+ --- The test result it was reported within.
+ idTestResult INTEGER REFERENCES TestResults(idTestResult) NOT NULL,
+ --- The test set this value is a part of (for avoiding joining thru TestResults).
+ -- Note! This is a foreign key, but we have to add it after TestSets has
+ -- been created, see further down.
+ idTestSet INTEGER NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The name.
+ idStrName INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL,
+ --- The value.
+ lValue bigint NOT NULL,
+ --- The unit.
+ -- @todo This is currently not defined properly. Will fix/correlate this
+ -- with the other places we use unit (IPRT/testdriver/VMMDev).
+ iUnit smallint NOT NULL CHECK (iUnit >= 0 AND iUnit < 1024)
+);
+
+CREATE INDEX TestResultValuesIdx ON TestResultValues(idTestResult);
+-- The TestResultValuesGraphIdx is for speeding up the result graph & reporting code.
+CREATE INDEX TestResultValuesGraphIdx ON TestResultValues(idStrName, tsCreated);
+-- The TestResultValuesLogIdx is for speeding up the log viewer.
+CREATE INDEX TestResultValuesLogIdx ON TestResultValues(idTestSet, tsCreated);
+
+
+--- @table TestResultFiles
+-- Test result files.
+--
+-- A testdriver or subordinate may report a file by using
+-- reporter.addFile() or reporter.addLogFile().
+--
+-- The files stored here as well as the primary log file will be processed by a
+-- batch job and compressed if considered compressable. Thus, TM will look for
+-- files with a .gz/.bz2 suffix first and then without a suffix.
+--
+-- This is an insert only table, no deletes, no updates.
+--
+CREATE SEQUENCE TestResultFileId
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestResultFiles (
+ --- The ID of this file.
+ idTestResultFile INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultFileId'),
+ --- The test result it was reported within.
+ idTestResult INTEGER REFERENCES TestResults(idTestResult) NOT NULL,
+ --- The test set this file is a part of (for avoiding joining thru TestResults).
+ -- Note! This is a foreign key, but we have to add it after TestSets has
+ -- been created, see further down.
+ idTestSet INTEGER NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The filename relative to TestSets(sBaseFilename) + '-'.
+ -- The set of valid filename characters should be very limited so that no
+ -- file system issues can occure either on the TM side or the user when
+ -- loading the files. Tests trying to use other characters will fail.
+ -- Valid character regular expession: '^[a-zA-Z0-9_-(){}#@+,.=]*$'
+ idStrFile INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL,
+ --- The description.
+ idStrDescription INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL,
+ --- The kind of file.
+ -- For instance: 'log/release/vm',
+ -- 'screenshot/failure',
+ -- 'screencapture/failure',
+ -- 'xmllog/somestuff'
+ idStrKind INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL,
+ --- The mime type for the file.
+ -- For instance: 'text/plain',
+ -- 'image/png',
+ -- 'video/webm',
+ -- 'text/xml'
+ idStrMime INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL
+);
+
+CREATE INDEX TestResultFilesIdx ON TestResultFiles(idTestResult);
+CREATE INDEX TestResultFilesIdx2 ON TestResultFiles(idTestSet, tsCreated DESC);
+
+
+--- @table TestResultMsgs
+-- Test result message.
+--
+-- A testdriver or subordinate may report a message via the sDetails parameter
+-- of the reporter.testFailure() method, while IPRT test cases will use
+-- RTTestFailed, RTTestPrintf and their friends. For RTTestPrintf, we will
+-- ignore the more verbose message levels since these can also be found in one
+-- of the logs.
+--
+-- This is an insert only table, no deletes, no updates.
+--
+CREATE TYPE TestResultMsgLevel_T AS ENUM (
+ 'failure',
+ 'info'
+);
+CREATE SEQUENCE TestResultMsgIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestResultMsgs (
+ --- The ID of this file.
+ idTestResultMsg INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultMsgIdSeq'),
+ --- The test result it was reported within.
+ idTestResult INTEGER REFERENCES TestResults(idTestResult) NOT NULL,
+ --- The test set this file is a part of (for avoiding joining thru TestResults).
+ -- Note! This is a foreign key, but we have to add it after TestSets has
+ -- been created, see further down.
+ idTestSet INTEGER NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The message string.
+ idStrMsg INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL,
+ --- The message level.
+ enmLevel TestResultMsgLevel_T NOT NULL
+);
+
+CREATE INDEX TestResultMsgsIdx ON TestResultMsgs(idTestResult);
+CREATE INDEX TestResultMsgsIdx2 ON TestResultMsgs(idTestSet, tsCreated DESC);
+
+
+--- @table TestSets
+-- Test sets / Test case runs.
+--
+-- This is where we collect data about test runs.
+--
+-- @todo Not entirely sure where the 'test set' term came from. Consider
+-- finding something more appropriate.
+--
+CREATE SEQUENCE TestSetIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestSets (
+ --- The ID of this test set.
+ idTestSet INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestSetIdSeq') NOT NULL,
+
+ --- The test config timestamp, used when reading test config.
+ tsConfig TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ --- When this test set was scheduled.
+ -- idGenTestBox is valid at this point.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ --- When this test completed, i.e. testing stopped. This should only be set once.
+ tsDone TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+ --- The current status.
+ enmStatus TestStatus_T DEFAULT 'running'::TestStatus_T NOT NULL,
+
+ --- The build we're testing.
+ -- Non-unique foreign key: Builds(idBuild)
+ idBuild INTEGER NOT NULL,
+ --- The build category of idBuild when the test started.
+ -- This is for speeding up graph data collection, i.e. avoid idBuild
+ -- the WHERE part of the selection.
+ idBuildCategory INTEGER REFERENCES BuildCategories(idBuildCategory) NOT NULL,
+ --- The test suite build we're using to do the testing.
+ -- This is NULL if the test suite zip wasn't referred or if a test suite
+ -- build source wasn't configured.
+ -- Non-unique foreign key: Builds(idBuild)
+ idBuildTestSuite INTEGER DEFAULT NULL,
+
+ --- The exact testbox configuration.
+ idGenTestBox INTEGER REFERENCES TestBoxes(idGenTestBox) NOT NULL,
+ --- The testbox ID for joining with (valid: tsStarted).
+ -- Non-unique foreign key: TestBoxes(idTestBox)
+ idTestBox INTEGER NOT NULL,
+ --- The scheduling group ID the test was scheduled thru (valid: tsStarted).
+ -- Non-unique foreign key: SchedGroups(idSchedGroup)
+ idSchedGroup INTEGER NOT NULL,
+
+ --- The testgroup (valid: tsConfig).
+ -- Non-unique foreign key: TestBoxes(idTestGroup)
+ -- Note! This also gives the member ship entry, since a testcase can only
+ -- have one membership per test group.
+ idTestGroup INTEGER NOT NULL,
+
+ --- The exact test case config we executed in this test run.
+ idGenTestCase INTEGER REFERENCES TestCases(idGenTestCase) NOT NULL,
+ --- The test case ID for joining with (valid: tsConfig).
+ -- Non-unique foreign key: TestBoxes(idTestCase)
+ idTestCase INTEGER NOT NULL,
+
+ --- The arguments (and requirements++) we executed this test case with.
+ idGenTestCaseArgs INTEGER REFERENCES TestCaseArgs(idGenTestCaseArgs) NOT NULL,
+ --- The argument variation ID (valid: tsConfig).
+ -- Non-unique foreign key: TestCaseArgs(idTestCaseArgs)
+ idTestCaseArgs INTEGER NOT NULL,
+
+ --- The root of the test result tree.
+ -- @note This will only be NULL early in the transaction setting up the testset.
+ -- @note If the test reports more than one top level test result, we'll
+ -- fail the whole test run and let the test developer fix it.
+ idTestResult INTEGER REFERENCES TestResults(idTestResult) DEFAULT NULL,
+
+ --- The base filename used for storing files related to this test set.
+ -- This is a path relative to wherever TM is dumping log files. In order
+ -- to not become a file system test case, we will try not to put too many
+ -- hundred thousand files in a directory. A simple first approach would
+ -- be to just use the current date (tsCreated) like this:
+ -- TM_FILE_DIR/year/month/day/TestSets.idTestSet
+ --
+ -- The primary log file for the test is this name suffixed by '.log'.
+ --
+ -- The files in the testresultfile table gets their full names like this:
+ -- TM_FILE_DIR/sBaseFilename-testresultfile.id-TestResultStrTab(testresultfile.idStrFilename)
+ --
+ -- @remarks We store this explicitly in case we change the directly layout
+ -- at some later point.
+ sBaseFilename text UNIQUE NOT NULL,
+
+ --- The gang member number number, 0 is the leader.
+ iGangMemberNo SMALLINT DEFAULT 0 NOT NULL CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024),
+ --- The test set of the gang leader, NULL if no gang involved.
+ -- @note This is set by the gang leader as well, so that we can find all
+ -- gang members by WHERE idTestSetGangLeader = :id.
+ idTestSetGangLeader INTEGER REFERENCES TestSets(idTestSet) DEFAULT NULL
+
+);
+CREATE INDEX TestSetsGangIdx ON TestSets (idTestSetGangLeader);
+CREATE INDEX TestSetsBoxIdx ON TestSets (idTestBox, idTestResult);
+CREATE INDEX TestSetsBuildIdx ON TestSets (idBuild, idTestResult);
+CREATE INDEX TestSetsTestCaseIdx ON TestSets (idTestCase, idTestResult);
+CREATE INDEX TestSetsTestVarIdx ON TestSets (idTestCaseArgs, idTestResult);
+--- The TestSetsDoneCreatedBuildCatIdx is for testbox results, graph options and such.
+CREATE INDEX TestSetsDoneCreatedBuildCatIdx ON TestSets (tsDone DESC NULLS FIRST, tsCreated ASC, idBuildCategory);
+--- For graphs.
+CREATE INDEX TestSetsGraphBoxIdx ON TestSets (idTestBox, tsCreated DESC, tsDone ASC NULLS LAST, idBuildCategory, idTestCase);
+
+ALTER TABLE TestResults ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultFiles ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultMsgs ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultFailures ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+
+
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+--
+-- T e s t M a n g e r P e r s i s t e n t S t o r a g e
+--
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+--- @type TestBoxState_T
+-- TestBox state.
+--
+-- @todo Consider drawing a state diagram for this.
+--
+CREATE TYPE TestBoxState_T AS ENUM (
+ --- Nothing to do.
+ -- Prev: testing, gang-cleanup, rebooting, upgrading,
+ -- upgrading-and-rebooting, doing-special-cmd.
+ -- Next: testing, gang-gathering, rebooting, upgrading,
+ -- upgrading-and-rebooting, doing-special-cmd.
+ 'idle',
+ --- Executing a test.
+ -- Prev: idle
+ -- Next: idle
+ 'testing',
+
+ -- Gang scheduling statuses:
+ --- The gathering of a gang.
+ -- Prev: idle
+ -- Next: gang-gathering-timedout, gang-testing
+ 'gang-gathering',
+ --- The gathering timed out, the testbox needs to cleanup and move on.
+ -- Prev: gang-gathering
+ -- Next: idle
+ -- This is set on all gathered members by the testbox who triggers the
+ -- timeout.
+ 'gang-gathering-timedout',
+ --- The gang scheduling equivalent of 'testing'.
+ -- Prev: gang-gathering
+ -- Next: gang-cleanup
+ 'gang-testing',
+ --- Waiting for the other gang members to stop testing so that cleanups
+ -- can be performed and members safely rescheduled.
+ -- Prev: gang-testing
+ -- Next: idle
+ --
+ -- There are two resource clean up issues being targeted here:
+ -- 1. Global resources will be allocated by the leader when he enters the
+ -- 'gang-gathering' state. If the leader quits and frees the resource
+ -- while someone is still using it, bad things will happen. Imagine a
+ -- global resource without any access checks and relies exclusivly on
+ -- the TM doing its job.
+ -- 2. TestBox resource accessed by other gang members may also be used in
+ -- other tests. Should a gang member leave early and embark on a
+ -- testcase using the same resources, bad things will happen. Example:
+ -- Live migration. One partner leaves early because it detected some
+ -- fatal failure, the other one is still trying to connect to him.
+ -- The testbox is scheduled again on the same live migration testcase,
+ -- only with different arguments (VM config), it will try migrate using
+ -- the same TCP ports. Confusion ensues.
+ --
+ -- To figure out whether to remain in this status because someone is
+ -- still testing:
+ -- SELECT COUNT(*) FROM TestBoxStatuses, TestSets
+ -- WHERE TestSets.idTestSetGangLeader = :idGangLeader
+ -- AND TestSets.idTestBox = TestBoxStatuses.idTestBox
+ -- AND TestSets.idTestSet = TestBoxStatuses.idTestSet
+ -- AND TestBoxStatuses.enmState = 'gang-testing'::TestBoxState_T;
+ 'gang-cleanup',
+
+ -- Command related statuses (all command status changes comes from 'idle'
+ -- and goes back to 'idle'):
+ 'rebooting',
+ 'upgrading',
+ 'upgrading-and-rebooting',
+ 'doing-special-cmd'
+);
+
+--- @table TestBoxStatuses
+-- Testbox status table.
+--
+-- History is not planned on this table.
+--
+CREATE TABLE TestBoxStatuses (
+ --- The testbox.
+ idTestBox INTEGER PRIMARY KEY NOT NULL,
+ --- The testbox generation ID.
+ idGenTestBox INTEGER REFERENCES TestBoxes(idGenTestBox) NOT NULL,
+ --- When this status was last updated.
+ -- This is updated everytime the testbox talks to the test manager, thus it
+ -- can easily be used to find testboxes which has stopped responding.
+ --
+ -- This is used for timeout calculation during gang-gathering, so in that
+ -- scenario it won't be updated until the gang is gathered or we time out.
+ tsUpdated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The current state.
+ enmState TestBoxState_T DEFAULT 'idle'::TestBoxState_T NOT NULL,
+ --- Reference to the test set
+ idTestSet INTEGER REFERENCES TestSets(idTestSet),
+ --- Interal work item number.
+ -- This is used to pick and prioritize between multiple scheduling groups.
+ iWorkItem INTEGER DEFAULT 0 NOT NULL
+);
+
+
+--- @table GlobalResourceStatuses
+-- Global resource status, tracks which test set resources are allocated by.
+--
+-- History is not planned on this table.
+--
+CREATE TABLE GlobalResourceStatuses (
+ --- The resource ID.
+ -- Non-unique foreign key: GlobalResources(idGlobalRsrc).
+ idGlobalRsrc INTEGER PRIMARY KEY NOT NULL,
+ --- The resource owner.
+ -- @note This is going thru testboxstatus to be able to use the testbox ID
+ -- as a foreign key.
+ idTestBox INTEGER REFERENCES TestBoxStatuses(idTestBox) NOT NULL,
+ --- When the allocation took place.
+ tsAllocated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL
+);
+
+
+--- @table SchedQueues
+-- Scheduler queue.
+--
+-- The queues are currently associated with a scheduling group, it could
+-- alternative be changed to hook on to a testbox instead. It depends on what
+-- kind of scheduling method we prefer. The former method aims at test case
+-- thruput, making sacrifices in the hardware distribution area. The latter is
+-- more like the old buildbox style testing, making sure that each test case is
+-- executed on each testbox.
+--
+-- When there are configuration changes, TM will regenerate the scheduling
+-- queue for the affected scheduling groups. We do not concern ourselves with
+-- trying to continue at the approximately same queue position, we simply take
+-- it from the top.
+--
+-- When a testbox ask for work, we will open a cursor on the queue and take the
+-- first test in the queue that can be executed on that testbox. The test will
+-- be moved to the end of the queue (getting a new item_id).
+--
+-- If a test is manually changed to the head of the queue, the item will get a
+-- item_id which is 1 lower than the head of the queue. Unless someone does
+-- this a couple of billion times, we shouldn't have any trouble running out of
+-- number space. :-)
+--
+-- Manually moving a test to the end of the queue is easy, just get a new
+-- 'item_id'.
+--
+-- History is not planned on this table.
+--
+CREATE SEQUENCE SchedQueueItemIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE SchedQueues (
+ --- The scheduling queue (one queue per scheduling group).
+ -- Non-unique foreign key: SchedGroups(idSchedGroup)
+ idSchedGroup INTEGER NOT NULL,
+ --- The scheduler queue entry ID.
+ -- Lower numbers means early queue position.
+ idItem INTEGER DEFAULT NEXTVAL('SchedQueueItemIdSeq') NOT NULL,
+ --- The queue offset.
+ -- This is used for repositining the queue when recreating it. It can also
+ -- be used to figure out how jumbled the queue gets after real life has had
+ -- it's effect on it.
+ offQueue INTEGER NOT NULL,
+ --- The test case argument variation to execute.
+ idGenTestCaseArgs INTEGER REFERENCES TestCaseArgs(idGenTestCaseArgs) NOT NULL,
+ --- The relevant testgroup.
+ -- Non-unique foreign key: TestGroups(idTestGroup).
+ idTestGroup INTEGER NOT NULL,
+ --- Aggregated test group dependencies (NULL if none).
+ -- Non-unique foreign key: TestGroups(idTestGroup).
+ -- See also comments on SchedGroupMembers.idTestGroupPreReq.
+ aidTestGroupPreReqs INTEGER ARRAY DEFAULT NULL,
+ --- The scheduling time constraints (see SchedGroupMembers.bmHourlySchedule).
+ bmHourlySchedule bit(168) DEFAULT NULL,
+ --- When the queue entry was created and for which config is valid.
+ -- This is the timestamp that should be used when reading config info.
+ tsConfig TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ --- When this status was last scheduled.
+ -- This is set to current_timestamp when moving the entry to the end of the
+ -- queue. It's initial value is unix-epoch. Not entirely sure if it's
+ -- useful beyond introspection and non-unique foreign key hacking.
+ tsLastScheduled TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'epoch' NOT NULL,
+
+ --- This is used in gang scheduling.
+ idTestSetGangLeader INTEGER REFERENCES TestSets(idTestSet) DEFAULT NULL UNIQUE,
+ --- The number of gang members still missing.
+ --
+ -- This saves calculating the number of missing members via selects like:
+ -- SELECT COUNT(*) FROM TestSets WHERE idTestSetGangLeader = :idGang;
+ -- and
+ -- SELECT cGangMembers FROM TestCaseArgs WHERE idGenTestCaseArgs = :idTest;
+ -- to figure out whether to remain in 'gather-gang'::TestBoxState_T.
+ --
+ cMissingGangMembers smallint DEFAULT 1 NOT NULL,
+
+ --- @todo
+ --- The number of times this has been considered for scheduling.
+ -- cConsidered SMALLINT DEFAULT 0 NOT NULL,
+
+ PRIMARY KEY (idSchedGroup, idItem)
+);
+CREATE INDEX SchedQueuesItemIdx ON SchedQueues(idItem);
+CREATE INDEX SchedQueuesSchedGroupIdx ON SchedQueues(idSchedGroup);
+
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseMap.png b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseMap.png
new file mode 100644
index 00000000..861a407d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseMap.png
Binary files differ
diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerVBoxPilot-1.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerVBoxPilot-1.pgsql
new file mode 100644
index 00000000..bdff3bc4
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/TestManagerVBoxPilot-1.pgsql
@@ -0,0 +1,101 @@
+-- $Id: TestManagerVBoxPilot-1.pgsql $
+--- @file
+-- VBox Test Manager - Setup for the 1st VBox Pilot.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+
+\set ON_ERROR_STOP 1
+\connect testmanager;
+
+BEGIN WORK;
+
+--
+-- The user we assign all the changes too.
+--
+INSERT INTO Users (sUsername, sEmail, sFullName, sLoginName)
+ VALUES ('vbox-pilot-config', 'pilot1@example.org', 'VBox Pilot Configurator', 'vbox-pilot-config');
+\set idUserQuery '(SELECT uid FROM Users WHERE sUsername = \'vbox-pilot-config\')'
+
+--
+-- Configure a scheduling group with build sources.
+--
+INSERT INTO BuildSources (uidAuthor, sName, sProduct, sBranch, asTypes, asOsArches)
+ VALUES (:idUserQuery, 'VBox trunk builds', 'VirtualBox', 'trunk', ARRAY['release', 'strict'], NULL);
+
+INSERT INTO BuildSources (uidAuthor, sName, sProduct, sBranch, asTypes, asOsArches)
+ VALUES (:idUserQuery, 'VBox TestSuite trunk builds', 'VBox TestSuite', 'trunk', ARRAY['release'], NULL);
+
+INSERT INTO SchedGroups (sName, sDescription, fEnabled, idBuildSrc, idBuildSrcTestSuite)
+ VALUES ('VirtualBox Trunk', NULL, TRUE,
+ (SELECT idBuildSrc FROM BuildSources WHERE sName = 'VBox trunk builds'),
+ (SELECT idBuildSrc FROM BuildSources WHERE sName = 'VBox TestSuite trunk builds') );
+\set idSchedGroupQuery '(SELECT idSchedGroup FROM SchedGroups WHERE sName = \'VirtualBox Trunk\')'
+
+--
+-- Configure three test groups.
+--
+INSERT INTO TestGroups (uidAuthor, sName)
+ VALUES (:idUserQuery, 'VBox smoketests');
+\set idGrpSmokeQuery '(SELECT idTestGroup FROM TestGroups WHERE sName = \'VBox smoketests\')'
+INSERT INTO SchedGroupMembers (idSchedGroup, idTestGroup, uidAuthor, idTestGroupPreReq)
+ VALUES (:idSchedGroupQuery, :idGrpSmokeQuery, :idUserQuery, NULL);
+
+INSERT INTO TestGroups (uidAuthor, sName)
+ VALUES (:idUserQuery, 'VBox general');
+\set idGrpGeneralQuery '(SELECT idTestGroup FROM TestGroups WHERE sName = \'VBox general\')'
+INSERT INTO SchedGroupMembers (idSchedGroup, idTestGroup, uidAuthor, idTestGroupPreReq)
+ VALUES (:idSchedGroupQuery, :idGrpGeneralQuery, :idUserQuery, :idGrpSmokeQuery);
+
+INSERT INTO TestGroups (uidAuthor, sName)
+ VALUES (:idUserQuery, 'VBox benchmarks');
+\set idGrpBenchmarksQuery '(SELECT idTestGroup FROM TestGroups WHERE sName = \'VBox benchmarks\')'
+INSERT INTO SchedGroupMembers (idSchedGroup, idTestGroup, uidAuthor, idTestGroupPreReq)
+ VALUES (:idSchedGroupQuery, :idGrpBenchmarksQuery, :idUserQuery, :idGrpGeneralQuery);
+
+
+--
+-- Testcases
+--
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (:idUserQuery, 'VBox install', TRUE, 600,
+ 'validationkit/testdriver/vboxinstaller.py --vbox-build @BUILD_BINARIES@ @ACTION@ -- testdriver/base.py @ACTION@',
+ '@VALIDATIONKIT_ZIP@');
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'VBox install'), :idUserQuery, '');
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES (:idGrpSmokeQuery, (SELECT idTestCase FROM TestCases WHERE sName = 'VBox install'), :idUserQuery);
+
+COMMIT WORK;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/gen-sql-comments.py b/src/VBox/ValidationKit/testmanager/db/gen-sql-comments.py
new file mode 100755
index 00000000..2bdc239c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/gen-sql-comments.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: gen-sql-comments.py $
+
+"""
+Converts doxygen style comments in SQL script to COMMENT ON statements.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+
+import sys;
+import re;
+
+
+def errorMsg(sMsg):
+ sys.stderr.write('error: %s\n' % (sMsg,));
+ return 1;
+
+class SqlDox(object):
+ """
+ Class for parsing relevant comments out of a pgsql file
+ and emit COMMENT ON statements from it.
+ """
+
+ def __init__(self, oFile, sFilename):
+ self.oFile = oFile;
+ self.sFilename = sFilename;
+ self.iLine = 0; # The current input line number.
+ self.sComment = None; # The current comment.
+ self.fCommentComplete = False; # Indicates that the comment has ended.
+ self.sCommentSqlObj = None; # SQL object indicated by the comment (@table).
+ self.sOuterSqlObj = None; # Like 'table yyyy' or 'type zzzz'.
+ self.sPrevSqlObj = None; # Like 'table xxxx'.
+
+
+ def error(self, sMsg):
+ return errorMsg('%s(%d): %s' % (self.sFilename, self.iLine, sMsg,));
+
+ def dprint(self, sMsg):
+ sys.stderr.write('debug: %s\n' % (sMsg,));
+ return True;
+
+ def resetComment(self):
+ self.sComment = None;
+ self.fCommentComplete = False;
+ self.sCommentSqlObj = None;
+
+ def quoteSqlString(self, s):
+ return s.replace("'", "''");
+
+ def commitComment2(self, sSqlObj):
+ if self.sComment is not None and sSqlObj is not None:
+ print("COMMENT ON %s IS\n '%s';\n" % (sSqlObj, self.quoteSqlString(self.sComment.strip())));
+ self.resetComment();
+ return True;
+
+ def commitComment(self):
+ return self.commitComment2(self.sCommentSqlObj);
+
+ def process(self):
+ for sLine in self.oFile:
+ self.iLine += 1;
+
+ sLine = sLine.strip();
+ self.dprint('line %d: %s\n' % (self.iLine, sLine));
+ if sLine.startswith('--'):
+ if sLine.startswith('--- '):
+ #
+ # New comment.
+ # The first list may have a @table, @type or similar that we're interested in.
+ #
+ self.commitComment();
+
+ sLine = sLine.lstrip('- ');
+ if sLine.startswith('@table '):
+ self.sCommentSqlObj = 'TABLE ' + (sLine[7:]).rstrip();
+ self.sComment = '';
+ elif sLine.startswith('@type '):
+ self.sCommentSqlObj = 'TYPE ' + (sLine[6:]).rstrip();
+ self.sComment = '';
+ elif sLine.startswith('@todo') \
+ or sLine.startswith('@file') \
+ or sLine.startswith('@page') \
+ or sLine.startswith('@name') \
+ or sLine.startswith('@{') \
+ or sLine.startswith('@}'):
+ # Ignore.
+ pass;
+ elif sLine.startswith('@'):
+ return self.error('Unknown tag: %s' % (sLine,));
+ else:
+ self.sComment = sLine;
+
+ elif (sLine.startswith('-- ') or sLine == '--') \
+ and self.sComment is not None and self.fCommentComplete is False:
+ #
+ # Append line to comment.
+ #
+ if sLine == '--':
+ sLine = '';
+ else:
+ sLine = (sLine[3:]);
+ if self.sComment == '':
+ self.sComment = sLine;
+ else:
+ self.sComment += "\n" + sLine;
+
+ elif sLine.startswith('--< '):
+ #
+ # Comment that starts on the same line as the object it describes.
+ #
+ sLine = (sLine[4:]).rstrip();
+ # => Later/never.
+ else:
+ #
+ # Not a comment that interests us. So, complete any open
+ # comment and commit it if we know which SQL object it
+ # applies to.
+ #
+ self.fCommentComplete = True;
+ if self.sCommentSqlObj is not None:
+ self.commitComment();
+ else:
+ #
+ # Not a comment. As above, we complete and optionally commit
+ # any open comment.
+ #
+ self.fCommentComplete = True;
+ if self.sCommentSqlObj is not None:
+ self.commitComment();
+
+ #
+ # Check for SQL (very fuzzy and bad).
+ #
+ asWords = sLine.split(' ');
+ if len(asWords) >= 3 \
+ and asWords[0] == 'CREATE':
+ # CREATE statement.
+ sType = asWords[1];
+ sName = asWords[2];
+ if sType == 'UNIQUE' and sName == 'INDEX' and len(asWords) >= 4:
+ sType = asWords[2];
+ sName = asWords[3];
+ if sType in ('TABLE', 'TYPE', 'INDEX', 'VIEW'):
+ self.sOuterSqlObj = sType + ' ' + sName;
+ self.sPrevSqlObj = self.sOuterSqlObj;
+ self.dprint('%s' % (self.sOuterSqlObj,));
+ self.commitComment2(self.sOuterSqlObj);
+ elif len(asWords) >= 1 \
+ and self.sOuterSqlObj is not None \
+ and self.sOuterSqlObj.startswith('TABLE ') \
+ and re.search("^(as|al|bm|c|enm|f|i|l|s|ts|uid|uuid)[A-Z][a-zA-Z0-9]*$", asWords[0]) is not None:
+ # Possibly a column name.
+ self.sPrevSqlObj = 'COLUMN ' + self.sOuterSqlObj[6:] + '.' + asWords[0];
+ self.dprint('column? %s' % (self.sPrevSqlObj));
+ self.commitComment2(self.sPrevSqlObj);
+
+ #
+ # Check for semicolon.
+ #
+ if sLine.find(");") >= 0:
+ self.sOuterSqlObj = None;
+
+ return 0;
+
+
+def usage():
+ sys.stderr.write('usage: gen-sql-comments.py <filename.pgsql>\n'
+ '\n'
+ 'The output goes to stdout.\n');
+ return 0;
+
+
+def main(asArgs):
+ # Parse the argument. :-)
+ sInput = None;
+ if (len(asArgs) != 2):
+ sys.stderr.write('syntax error: expected exactly 1 argument, a psql file\n');
+ usage();
+ return 2;
+ sInput = asArgs[1];
+
+ # Do the job, outputting to standard output.
+ try:
+ oFile = open(sInput, 'r');
+ except:
+ return errorMsg("failed to open '%s' for reading" % (sInput,));
+
+ # header.
+ print("-- $" "Id" "$");
+ print("--- @file");
+ print("-- Autogenerated from %s. Do not edit!" % (sInput,));
+ print("--");
+ print("");
+ for sLine in __copyright__.split('\n'):
+ if len(sLine) > 0:
+ print("-- %s" % (sLine,));
+ else:
+ print("--");
+ print("");
+ print("");
+ me = SqlDox(oFile, sInput);
+ return me.process();
+
+sys.exit(main(sys.argv));
+
diff --git a/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py b/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py
new file mode 100755
index 00000000..6676de47
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py
@@ -0,0 +1,392 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: partial-db-dump.py $
+# pylint: disable=line-too-long
+
+"""
+Utility for dumping the last X days of data.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports
+import sys;
+import os;
+import zipfile;
+from optparse import OptionParser;
+import xml.etree.ElementTree as ET;
+
+# Add Test Manager's modules path
+g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
+sys.path.append(g_ksTestManagerDir);
+
+# Test Manager imports
+from testmanager.core.db import TMDatabaseConnection;
+from common import utils;
+
+
+class PartialDbDump(object): # pylint: disable=too-few-public-methods
+ """
+ Dumps or loads the last X days of database data.
+
+ This is a useful tool when hacking on the test manager locally. You can get
+ a small sample from the last few days from the production test manager server
+ without spending hours dumping, downloading, and loading the whole database
+ (because it is gigantic).
+
+ """
+
+ def __init__(self):
+ """
+ Parse command line.
+ """
+
+ oParser = OptionParser()
+ oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true',
+ help = 'Quiet execution');
+ oParser.add_option('-f', '--filename', dest = 'sFilename', metavar = '<filename>',
+ default = 'partial-db-dump.zip', help = 'The name of the partial database zip file to write/load.');
+
+ oParser.add_option('-t', '--tmp-file', dest = 'sTempFile', metavar = '<temp-file>',
+ default = '/tmp/tm-partial-db-dump.pgtxt',
+ help = 'Name of temporary file for duping tables. Must be absolute');
+ oParser.add_option('--days-to-dump', dest = 'cDays', metavar = '<days>', type = 'int', default = 14,
+ help = 'How many days to dump (counting backward from current date).');
+ oParser.add_option('--load-dump-into-database', dest = 'fLoadDumpIntoDatabase', action = 'store_true',
+ default = False, help = 'For loading instead of dumping.');
+ oParser.add_option('--store', dest = 'fStore', action = 'store_true',
+ default = False, help = 'Do not compress the zip file.');
+
+ (self.oConfig, _) = oParser.parse_args();
+
+
+ ##
+ # Tables dumped in full because they're either needed in full or they normally
+ # aren't large enough to bother reducing.
+ kasTablesToDumpInFull = [
+ 'Users',
+ 'BuildBlacklist',
+ 'BuildCategories',
+ 'BuildSources',
+ 'FailureCategories',
+ 'FailureReasons',
+ 'GlobalResources',
+ 'Testcases',
+ 'TestcaseArgs',
+ 'TestcaseDeps',
+ 'TestcaseGlobalRsrcDeps',
+ 'TestGroups',
+ 'TestGroupMembers',
+ 'SchedGroups',
+ 'SchedGroupMembers', # ?
+ 'TestBoxesInSchedGroups', # ?
+ 'SchedQueues',
+ 'TestResultStrTab', # 36K rows, never mind complicated then.
+ ];
+
+ ##
+ # Tables where we only dump partial info (the TestResult* tables are rather
+ # gigantic).
+ kasTablesToPartiallyDump = [
+ 'TestBoxes', # 2016-05-25: ca. 641 MB
+ 'TestSets', # 2016-05-25: ca. 525 MB
+ 'TestResults', # 2016-05-25: ca. 13 GB
+ 'TestResultFiles', # 2016-05-25: ca. 87 MB
+ 'TestResultMsgs', # 2016-05-25: ca. 29 MB
+ 'TestResultValues', # 2016-05-25: ca. 3728 MB
+ 'TestResultFailures',
+ 'Builds',
+ 'TestBoxStrTab',
+ 'SystemLog',
+ 'VcsRevisions',
+ ];
+
+ def _doCopyTo(self, sTable, oZipFile, oDb, sSql, aoArgs = None):
+ """ Does one COPY TO job. """
+ print('Dumping %s...' % (sTable,));
+
+ if aoArgs is not None:
+ sSql = oDb.formatBindArgs(sSql, aoArgs);
+
+ oFile = open(self.oConfig.sTempFile, 'w');
+ oDb.copyExpert(sSql, oFile);
+ cRows = oDb.getRowCount();
+ oFile.close();
+ print('... %s rows.' % (cRows,));
+
+ oZipFile.write(self.oConfig.sTempFile, sTable);
+ return True;
+
+ def _doDump(self, oDb):
+ """ Does the dumping of the database. """
+
+ enmCompression = zipfile.ZIP_DEFLATED;
+ if self.oConfig.fStore:
+ enmCompression = zipfile.ZIP_STORED;
+ oZipFile = zipfile.ZipFile(self.oConfig.sFilename, 'w', enmCompression);
+
+ oDb.begin();
+
+ # Dumping full tables is simple.
+ for sTable in self.kasTablesToDumpInFull:
+ self._doCopyTo(sTable, oZipFile, oDb, 'COPY ' + sTable + ' TO STDOUT WITH (FORMAT TEXT)');
+
+ # Figure out how far back we need to go.
+ oDb.execute('SELECT CURRENT_TIMESTAMP - INTERVAL \'%s days\'' % (self.oConfig.cDays,));
+ tsEffective = oDb.fetchOne()[0];
+ oDb.execute('SELECT CURRENT_TIMESTAMP - INTERVAL \'%s days\'' % (self.oConfig.cDays + 2,));
+ tsEffectiveSafe = oDb.fetchOne()[0];
+ print('Going back to: %s (safe: %s)' % (tsEffective, tsEffectiveSafe));
+
+ # We dump test boxes back to the safe timestamp because the test sets may
+ # use slightly dated test box references and we don't wish to have dangling
+ # references when loading.
+ for sTable in [ 'TestBoxes', ]:
+ self._doCopyTo(sTable, oZipFile, oDb,
+ 'COPY (SELECT * FROM ' + sTable + ' WHERE tsExpire >= %s) TO STDOUT WITH (FORMAT TEXT)',
+ (tsEffectiveSafe,));
+
+ # The test results needs to start with test sets and then dump everything
+ # releated to them. So, figure the lowest (oldest) test set ID we'll be
+ # dumping first.
+ oDb.execute('SELECT idTestSet FROM TestSets WHERE tsCreated >= %s', (tsEffective, ));
+ idFirstTestSet = 0;
+ if oDb.getRowCount() > 0:
+ idFirstTestSet = oDb.fetchOne()[0];
+ print('First test set ID: %s' % (idFirstTestSet,));
+
+ oDb.execute('SELECT MAX(idTestSet) FROM TestSets WHERE tsCreated >= %s', (tsEffective, ));
+ idLastTestSet = 0;
+ if oDb.getRowCount() > 0:
+ idLastTestSet = oDb.fetchOne()[0];
+ print('Last test set ID: %s' % (idLastTestSet,));
+
+ oDb.execute('SELECT MAX(idTestResult) FROM TestResults WHERE tsCreated >= %s', (tsEffective, ));
+ idLastTestResult = 0;
+ if oDb.getRowCount() > 0:
+ idLastTestResult = oDb.fetchOne()[0];
+ print('Last test result ID: %s' % (idLastTestResult,));
+
+ # Tables with idTestSet member.
+ for sTable in [ 'TestSets', 'TestResults', 'TestResultValues' ]:
+ self._doCopyTo(sTable, oZipFile, oDb,
+ 'COPY (SELECT *\n'
+ ' FROM ' + sTable + '\n'
+ ' WHERE idTestSet >= %s\n'
+ ' AND idTestSet <= %s\n'
+ ' AND idTestResult <= %s\n'
+ ') TO STDOUT WITH (FORMAT TEXT)'
+ , ( idFirstTestSet, idLastTestSet, idLastTestResult,));
+
+ # Tables where we have to go via TestResult.
+ for sTable in [ 'TestResultFiles', 'TestResultMsgs', 'TestResultFailures' ]:
+ self._doCopyTo(sTable, oZipFile, oDb,
+ 'COPY (SELECT it.*\n'
+ ' FROM ' + sTable + ' it, TestResults tr\n'
+ ' WHERE tr.idTestSet >= %s\n'
+ ' AND tr.idTestSet <= %s\n'
+ ' AND tr.idTestResult <= %s\n'
+ ' AND tr.tsCreated >= %s\n' # performance hack.
+ ' AND it.idTestResult = tr.idTestResult\n'
+ ') TO STDOUT WITH (FORMAT TEXT)'
+ , ( idFirstTestSet, idLastTestSet, idLastTestResult, tsEffective,));
+
+ # Tables which goes exclusively by tsCreated using tsEffectiveSafe.
+ for sTable in [ 'SystemLog', 'VcsRevisions' ]:
+ self._doCopyTo(sTable, oZipFile, oDb,
+ 'COPY (SELECT * FROM ' + sTable + ' WHERE tsCreated >= %s) TO STDOUT WITH (FORMAT TEXT)',
+ (tsEffectiveSafe,));
+
+ # The builds table.
+ oDb.execute('SELECT MIN(idBuild), MIN(idBuildTestSuite) FROM TestSets WHERE idTestSet >= %s', (idFirstTestSet,));
+ idFirstBuild = 0;
+ if oDb.getRowCount() > 0:
+ idFirstBuild = min(oDb.fetchOne());
+ print('First build ID: %s' % (idFirstBuild,));
+ for sTable in [ 'Builds', ]:
+ self._doCopyTo(sTable, oZipFile, oDb,
+ 'COPY (SELECT * FROM ' + sTable + ' WHERE idBuild >= %s) TO STDOUT WITH (FORMAT TEXT)',
+ (idFirstBuild,));
+
+ # The test box string table.
+ self._doCopyTo('TestBoxStrTab', oZipFile, oDb, '''
+COPY (SELECT * FROM TestBoxStrTab WHERE idStr IN (
+ ( SELECT 0
+ ) UNION ( SELECT idStrComment FROM TestBoxes WHERE tsExpire >= %s
+ ) UNION ( SELECT idStrCpuArch FROM TestBoxes WHERE tsExpire >= %s
+ ) UNION ( SELECT idStrCpuName FROM TestBoxes WHERE tsExpire >= %s
+ ) UNION ( SELECT idStrCpuVendor FROM TestBoxes WHERE tsExpire >= %s
+ ) UNION ( SELECT idStrDescription FROM TestBoxes WHERE tsExpire >= %s
+ ) UNION ( SELECT idStrOS FROM TestBoxes WHERE tsExpire >= %s
+ ) UNION ( SELECT idStrOsVersion FROM TestBoxes WHERE tsExpire >= %s
+ ) UNION ( SELECT idStrReport FROM TestBoxes WHERE tsExpire >= %s
+ ) ) ) TO STDOUT WITH (FORMAT TEXT)
+''', (tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe,
+ tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe,));
+
+ oZipFile.close();
+ print('Done!');
+ return 0;
+
+ def _doLoad(self, oDb):
+ """ Does the loading of the dumped data into the database. """
+
+ try:
+ oZipFile = zipfile.ZipFile(self.oConfig.sFilename, 'r');
+ except:
+ print('error: Dump file "%s" cannot be opened! Use "-f <file>" to specify a file.' % (self.oConfig.sFilename,));
+ return 1;
+
+ asTablesInLoadOrder = [
+ 'Users',
+ 'BuildBlacklist',
+ 'BuildCategories',
+ 'BuildSources',
+ 'FailureCategories',
+ 'FailureReasons',
+ 'GlobalResources',
+ 'Testcases',
+ 'TestcaseArgs',
+ 'TestcaseDeps',
+ 'TestcaseGlobalRsrcDeps',
+ 'TestGroups',
+ 'TestGroupMembers',
+ 'SchedGroups',
+ 'TestBoxStrTab',
+ 'TestBoxes',
+ 'SchedGroupMembers',
+ 'TestBoxesInSchedGroups',
+ 'SchedQueues',
+ 'Builds',
+ 'SystemLog',
+ 'VcsRevisions',
+ 'TestResultStrTab',
+ 'TestSets',
+ 'TestResults',
+ 'TestResultFiles',
+ 'TestResultMsgs',
+ 'TestResultValues',
+ 'TestResultFailures',
+ ];
+ assert len(asTablesInLoadOrder) == len(self.kasTablesToDumpInFull) + len(self.kasTablesToPartiallyDump);
+
+ oDb.begin();
+ oDb.execute('SET CONSTRAINTS ALL DEFERRED;');
+
+ print('Checking if the database looks empty...\n');
+ for sTable in asTablesInLoadOrder + [ 'TestBoxStatuses', 'GlobalResourceStatuses' ]:
+ oDb.execute('SELECT COUNT(*) FROM ' + sTable);
+ cRows = oDb.fetchOne()[0];
+ cMaxRows = 0;
+ if sTable in [ 'SchedGroups', 'TestBoxStrTab', 'TestResultStrTab', 'Users' ]: cMaxRows = 1;
+ if cRows > cMaxRows:
+ print('error: Table %s has %u rows which is more than %u - refusing to delete and load.'
+ % (sTable, cRows, cMaxRows,));
+ print('info: Please drop and recreate the database before loading!');
+ return 1;
+
+ print('Dropping default table content...\n');
+ for sTable in [ 'SchedGroups', 'TestBoxStrTab', 'TestResultStrTab', 'Users']:
+ oDb.execute('DELETE FROM ' + sTable);
+
+ oDb.execute('ALTER TABLE TestSets DROP CONSTRAINT IF EXISTS TestSets_idTestResult_fkey');
+
+ for sTable in asTablesInLoadOrder:
+ print('Loading %s...' % (sTable,));
+ oFile = oZipFile.open(sTable);
+ oDb.copyExpert('COPY ' + sTable + ' FROM STDIN WITH (FORMAT TEXT)', oFile);
+ cRows = oDb.getRowCount();
+ print('... %s rows.' % (cRows,));
+
+ oDb.execute('ALTER TABLE TestSets ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult)');
+ oDb.commit();
+
+ # Correct sequences.
+ atSequences = [
+ ( 'UserIdSeq', 'Users', 'uid' ),
+ ( 'GlobalResourceIdSeq', 'GlobalResources', 'idGlobalRsrc' ),
+ ( 'BuildSourceIdSeq', 'BuildSources', 'idBuildSrc' ),
+ ( 'TestCaseIdSeq', 'TestCases', 'idTestCase' ),
+ ( 'TestCaseGenIdSeq', 'TestCases', 'idGenTestCase' ),
+ ( 'TestCaseArgsIdSeq', 'TestCaseArgs', 'idTestCaseArgs' ),
+ ( 'TestCaseArgsGenIdSeq', 'TestCaseArgs', 'idGenTestCaseArgs' ),
+ ( 'TestGroupIdSeq', 'TestGroups', 'idTestGroup' ),
+ ( 'SchedGroupIdSeq', 'SchedGroups', 'idSchedGroup' ),
+ ( 'TestBoxStrTabIdSeq', 'TestBoxStrTab', 'idStr' ),
+ ( 'TestBoxIdSeq', 'TestBoxes', 'idTestBox' ),
+ ( 'TestBoxGenIdSeq', 'TestBoxes', 'idGenTestBox' ),
+ ( 'FailureCategoryIdSeq', 'FailureCategories', 'idFailureCategory' ),
+ ( 'FailureReasonIdSeq', 'FailureReasons', 'idFailureReason' ),
+ ( 'BuildBlacklistIdSeq', 'BuildBlacklist', 'idBlacklisting' ),
+ ( 'BuildCategoryIdSeq', 'BuildCategories', 'idBuildCategory' ),
+ ( 'BuildIdSeq', 'Builds', 'idBuild' ),
+ ( 'TestResultStrTabIdSeq', 'TestResultStrTab', 'idStr' ),
+ ( 'TestResultIdSeq', 'TestResults', 'idTestResult' ),
+ ( 'TestResultValueIdSeq', 'TestResultValues', 'idTestResultValue' ),
+ ( 'TestResultFileId', 'TestResultFiles', 'idTestResultFile' ),
+ ( 'TestResultMsgIdSeq', 'TestResultMsgs', 'idTestResultMsg' ),
+ ( 'TestSetIdSeq', 'TestSets', 'idTestSet' ),
+ ( 'SchedQueueItemIdSeq', 'SchedQueues', 'idItem' ),
+ ];
+ for (sSeq, sTab, sCol) in atSequences:
+ oDb.execute('SELECT MAX(%s) FROM %s' % (sCol, sTab,));
+ idMax = oDb.fetchOne()[0];
+ print('%s: idMax=%s' % (sSeq, idMax));
+ if idMax is not None:
+ oDb.execute('SELECT setval(\'%s\', %s)' % (sSeq, idMax));
+
+ # Last step.
+ print('Analyzing...');
+ oDb.execute('ANALYZE');
+ oDb.commit();
+
+ print('Done!');
+ return 0;
+
+ def main(self):
+ """
+ Main function.
+ """
+ oDb = TMDatabaseConnection();
+
+ if self.oConfig.fLoadDumpIntoDatabase is not True:
+ rc = self._doDump(oDb);
+ else:
+ rc = self._doLoad(oDb);
+
+ oDb.close();
+ return 0;
+
+if __name__ == '__main__':
+ sys.exit(PartialDbDump().main());
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r01-builds-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r01-builds-1.pgsql
new file mode 100644
index 00000000..f8ab331d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r01-builds-1.pgsql
@@ -0,0 +1,91 @@
+-- $Id: tmdb-r01-builds-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Changed Builds to be historized.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+DROP TABLE OldBuilds;
+DROP TABLE NewBuilds;
+DROP INDEX BuildsLookupIdx;
+
+\set ON_ERROR_STOP 1
+
+--
+-- idBuild won't be unique, so it cannot be used directly as a foreign key
+-- by TestSets.
+--
+ALTER TABLE TestSets
+ DROP CONSTRAINT TestSets_idBuild_fkey;
+ALTER TABLE TestSets
+ DROP CONSTRAINT TestSets_idBuildTestSuite_fkey;
+
+
+--
+-- Create the table, filling it with the current Builds content.
+--
+CREATE TABLE NewBuilds (
+ idBuild INTEGER DEFAULT NEXTVAL('BuildIdSeq') NOT NULL,
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ uidAuthor INTEGER DEFAULT NULL,
+ idBuildCategory INTEGER REFERENCES BuildCategories(idBuildCategory) NOT NULL,
+ iRevision INTEGER NOT NULL,
+ sVersion TEXT NOT NULL,
+ sLogUrl TEXT,
+ sBinaries TEXT NOT NULL,
+ fBinariesDeleted BOOLEAN DEFAULT FALSE NOT NULL,
+ UNIQUE (idBuild, tsExpire)
+);
+
+INSERT INTO NewBuilds (idBuild, tsCreated, tsEffective, uidAuthor, idBuildCategory, iRevision, sVersion, sLogUrl, sBinaries)
+ SELECT idBuild, tsCreated, tsCreated, uidAuthor, idBuildCategory, iRevision, sVersion, sLogUrl, sBinaries
+ FROM Builds;
+COMMIT;
+
+-- Switch the tables.
+ALTER TABLE Builds RENAME TO OldBuilds;
+ALTER TABLE NewBuilds RENAME TO Builds;
+COMMIT;
+
+-- Finally index the table.
+CREATE INDEX BuildsLookupIdx ON Builds (idBuildCategory, iRevision);
+COMMIT;
+
+DROP TABLE OldBuilds;
+COMMIT;
+
+-- Fix implicit index name.
+ALTER INDEX newbuilds_idbuild_tsexpire_key RENAME TO builds_idbuild_tsexpire_key;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r02-testboxes-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r02-testboxes-1.pgsql
new file mode 100644
index 00000000..2cd75da0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r02-testboxes-1.pgsql
@@ -0,0 +1,194 @@
+-- $Id: tmdb-r02-testboxes-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds fCpu64BitGuest to TestBoxes
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+DROP TABLE OldTestBoxes;
+DROP TABLE NewTestBoxes;
+
+\d TestBoxes;
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE;
+
+DROP INDEX TestBoxesUuidIdx;
+
+--
+-- Rename the original table, drop constrains and foreign key references so we
+-- get the right name automatic when creating the new one.
+--
+ALTER TABLE TestBoxes RENAME TO OldTestBoxes;
+
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_ccpus_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbmemory_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbscratch_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pctscaletimeout_check;
+
+ALTER TABLE TestBoxStatuses DROP CONSTRAINT TestBoxStatuses_idGenTestBox_fkey;
+ALTER TABLE TestSets DROP CONSTRAINT TestSets_idGenTestBox_fkey;
+
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pkey;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_idgentestbox_key;
+
+--
+-- Create the new table, filling it with the current TestBoxes content.
+--
+CREATE TABLE TestBoxes (
+ --- The fixed testbox ID.
+ -- This is assigned when the testbox is created and will never change.
+ idTestBox INTEGER DEFAULT NEXTVAL('TestBoxIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- When modified automatically by the testbox, NULL is used.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER DEFAULT NULL,
+ --- Generation ID for this row.
+ -- This is primarily for referencing by TestSets.
+ idGenTestBox INTEGER UNIQUE DEFAULT NEXTVAL('TestBoxGenIdSeq') NOT NULL,
+
+ --- The testbox IP.
+ -- This is from the webserver point of view and automatically updated on
+ -- SIGNON. The test setup doesn't permit for IP addresses to change while
+ -- the testbox is operational, because this will break gang tests.
+ ip inet NOT NULL,
+ --- The system or firmware UUID.
+ -- This uniquely identifies the testbox when talking to the server. After
+ -- SIGNON though, the testbox will also provide idTestBox and ip to
+ -- establish its identity beyond doubt.
+ uuidSystem uuid NOT NULL,
+ --- The testbox name.
+ -- Usually similar to the DNS name.
+ sName text NOT NULL,
+ --- Optional testbox description.
+ -- Intended for describing the box as well as making other relevant notes.
+ sDescription text DEFAULT NULL,
+
+ --- Reference to the scheduling group that this testbox is a member of.
+ -- Non-unique foreign key: SchedGroups(idSchedGroup)
+ -- A testbox is always part of a group, the default one nothing else.
+ idSchedGroup INTEGER DEFAULT 1 NOT NULL,
+
+ --- Indicates whether this testbox is enabled.
+ -- A testbox gets disabled when we're doing maintenance, debugging a issue
+ -- that happens only on that testbox, or some similar stuff. This is an
+ -- alternative to deleting the testbox.
+ fEnabled BOOLEAN DEFAULT NULL,
+
+ --- The kind of lights-out-management.
+ enmLomKind LomKind_T DEFAULT 'none'::LomKind_T NOT NULL,
+ --- The IP adress of the lights-out-management.
+ -- This can be NULL if enmLomKind is 'none', otherwise it must contain a valid address.
+ ipLom inet DEFAULT NULL,
+
+ --- Timeout scale factor, given as a percent.
+ -- This is a crude adjustment of the test case timeout for slower hardware.
+ pctScaleTimeout smallint DEFAULT 100 NOT NULL CHECK (pctScaleTimeout > 10 AND pctScaleTimeout < 20000),
+
+ --- @name Scheduling properties (reported by testbox script).
+ -- @{
+ --- Same abbrieviations as kBuild, see KBUILD_OSES.
+ sOs text DEFAULT NULL,
+ --- Informational, no fixed format.
+ sOsVersion text DEFAULT NULL,
+ --- Same as CPUID reports (GenuineIntel, AuthenticAMD, CentaurHauls, ...).
+ sCpuVendor text DEFAULT NULL,
+ --- Same as kBuild - x86, amd64, ... See KBUILD_ARCHES.
+ sCpuArch text DEFAULT NULL,
+ --- Number of CPUs, CPU cores and CPU threads.
+ cCpus smallint DEFAULT NULL CHECK (cCpus IS NULL OR cCpus > 0),
+ --- Set if capable of hardware virtualization.
+ fCpuHwVirt boolean DEFAULT NULL,
+ --- Set if capable of nested paging.
+ fCpuNestedPaging boolean DEFAULT NULL,
+ --- Set if CPU capable of 64-bit (VBox) guests.
+ fCpu64BitGuest boolean DEFAULT NULL,
+ --- Set if chipset with usable IOMMU (VT-d / AMD-Vi).
+ fChipsetIoMmu boolean DEFAULT NULL,
+ --- The (approximate) memory size in megabytes (rounded down to nearest 4 MB).
+ cMbMemory bigint DEFAULT NULL CHECK (cMbMemory IS NULL OR cMbMemory > 0),
+ --- The amount of scratch space in megabytes (rounded down to nearest 64 MB).
+ cMbScratch bigint DEFAULT NULL CHECK (cMbScratch IS NULL OR cMbScratch >= 0),
+ --- @}
+
+ --- The testbox script revision number, serves the purpose of a version number.
+ -- Probably good to have when scheduling upgrades as well for status purposes.
+ iTestBoxScriptRev INTEGER DEFAULT 0 NOT NULL,
+ --- The python sys.hexversion (layed out as of 2.7).
+ -- Good to know which python versions we need to support.
+ iPythonHexVersion INTEGER DEFAULT NULL,
+
+ --- Pending command.
+ -- @note We put it here instead of in TestBoxStatuses to get history.
+ enmPendingCmd TestBoxCmd_T DEFAULT 'none'::TestBoxCmd_T NOT NULL,
+
+ PRIMARY KEY (idTestBox, tsExpire),
+
+ --- Nested paging requires hardware virtualization.
+ CHECK (fCpuNestedPaging IS NULL OR (fCpuNestedPaging <> TRUE OR fCpuHwVirt = TRUE))
+);
+
+
+INSERT INTO TestBoxes ( idTestBox, tsEffective, tsExpire, uidAuthor, idGenTestBox, ip, uuidSystem, sName, sDescription,
+ idSchedGroup, fEnabled, enmLomKind, ipLom, pctScaleTimeout, sOs, sOsVersion, sCpuVendor, sCpuArch, cCpus, fCpuHwVirt,
+ fCpuNestedPaging, fCpu64BitGuest, fChipsetIoMmu, cMbMemory, cMbScratch, iTestBoxScriptRev, iPythonHexVersion,
+ enmPendingCmd )
+ SELECT idTestBox, tsEffective, tsExpire, uidAuthor, idGenTestBox, ip, uuidSystem, sName, sDescription,
+ idSchedGroup, fEnabled, enmLomKind, ipLom, pctScaleTimeout, sOs, sOsVersion, sCpuVendor, sCpuArch, cCpus, fCpuHwVirt,
+ fCpuNestedPaging, TRUE, fChipsetIoMmu, cMbMemory, cMbScratch, iTestBoxScriptRev, iPythonHexVersion,
+ enmPendingCmd
+ FROM OldTestBoxes;
+
+-- Add index.
+CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire);
+
+-- Restore foreign key references to the table.
+ALTER TABLE TestBoxStatuses ADD CONSTRAINT TestBoxStatuses_idGenTestBox_fkey FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox);
+ALTER TABLE TestSets ADD CONSTRAINT TestSets_idGenTestBox_fkey FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox);
+
+-- Drop the old table.
+DROP TABLE OldTestBoxes;
+
+COMMIT;
+
+\d TestBoxes;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r03-teststatus-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r03-teststatus-1.pgsql
new file mode 100644
index 00000000..2eed9f3c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r03-teststatus-1.pgsql
@@ -0,0 +1,48 @@
+-- $Id: tmdb-r03-teststatus-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds 'bad-testbox', 'aborted', and 'timeout' to TestStatus_T.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 1
+
+\dT+ TestStatus_T
+
+ALTER TYPE TestStatus_T ADD VALUE 'bad-testbox' BEFORE 'failure';
+ALTER TYPE TestStatus_T ADD VALUE 'aborted' BEFORE 'failure';
+ALTER TYPE TestStatus_T ADD VALUE 'timed-out' AFTER 'failure';
+
+\dT+ TestStatus_T
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r04-teststatus-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r04-teststatus-2.pgsql
new file mode 100644
index 00000000..fa82d36b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r04-teststatus-2.pgsql
@@ -0,0 +1,46 @@
+-- $Id: tmdb-r04-teststatus-2.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds 'rebooted' to TestStatus_T.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 1
+
+\dT+ TestStatus_T
+
+ALTER TYPE TestStatus_T ADD VALUE 'rebooted' AFTER 'timed-out';
+
+\dT+ TestStatus_T
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r05-teststatus-3.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r05-teststatus-3.pgsql
new file mode 100644
index 00000000..87656ca9
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r05-teststatus-3.pgsql
@@ -0,0 +1,54 @@
+-- $Id: tmdb-r05-teststatus-3.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds 'rebooted' to TestStatus_T.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ TestResults
+
+ALTER TABLE TestResults
+ DROP CONSTRAINT CheckStatusMatchesErrors;
+ALTER TABLE TestResults
+ ADD CONSTRAINT CheckStatusMatchesErrors
+ CHECK ( (cErrors > 0 AND enmStatus IN ('running'::TestStatus_T,
+ 'failure'::TestStatus_T, 'timed-out'::TestStatus_T, 'rebooted'::TestStatus_T ))
+ OR (cErrors = 0 AND enmStatus IN ('running'::TestStatus_T, 'success'::TestStatus_T,
+ 'skipped'::TestStatus_T, 'aborted'::TestStatus_T, 'bad-testbox'::TestStatus_T))
+ );
+COMMIT;
+\d+ TestResults
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r06-buildsources-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r06-buildsources-1.pgsql
new file mode 100644
index 00000000..8b4213c0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r06-buildsources-1.pgsql
@@ -0,0 +1,46 @@
+-- $Id: tmdb-r06-buildsources-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds cMaxSecondsOld to BuildSources.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 1
+
+\d+ buildsources
+
+ALTER TABLE BuildSources ADD COLUMN cSecMaxAge INTEGER DEFAULT NULL;
+
+\d+ buildsources
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r07-testresults-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r07-testresults-1.pgsql
new file mode 100644
index 00000000..5ab20bc2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r07-testresults-1.pgsql
@@ -0,0 +1,47 @@
+-- $Id: tmdb-r07-testresults-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds an index to TestResults.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ TestResults
+
+CREATE INDEX TestResultsNameIdx ON TestResults (idStrName, idTestResult, tsCreated);
+COMMIT;
+
+\d+ TestResults
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r08-testresultvalues-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r08-testresultvalues-1.pgsql
new file mode 100644
index 00000000..5d963664
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r08-testresultvalues-1.pgsql
@@ -0,0 +1,47 @@
+-- $Id: tmdb-r08-testresultvalues-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds an index to TestResultValues.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ TestResultValues
+
+CREATE INDEX TestResultValuesNameIdx ON TestResultValues (idStrName, tsCreated);
+COMMIT;
+
+\d+ TestResultValues
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r09-testsets-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r09-testsets-1.pgsql
new file mode 100644
index 00000000..5f002dc4
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r09-testsets-1.pgsql
@@ -0,0 +1,48 @@
+-- $Id: tmdb-r09-testsets-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds two indexes to TestSets.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ TestSets
+
+CREATE INDEX TestSetsCreated ON TestSets (tsCreated);
+CREATE INDEX TestSetsDone ON TestSets (tsDone);
+COMMIT;
+
+\d+ TestSets
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r10-testresultvalues-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r10-testresultvalues-2.pgsql
new file mode 100644
index 00000000..ceb4a429
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r10-testresultvalues-2.pgsql
@@ -0,0 +1,111 @@
+-- $Id: tmdb-r10-testresultvalues-2.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds an idTestSet to TestResultValues.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Cleanup after failed runs.
+--
+DROP TABLE NewTestResultValues;
+
+--
+-- Drop all indexes (might already be dropped).
+--
+DROP INDEX TestResultValuesIdx;
+DROP INDEX TestResultValuesNameIdx;
+
+-- Die on error from now on.
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ TestResultValues;
+
+--
+-- Create the new version of the table and filling with the content of the old.
+--
+CREATE TABLE NewTestResultValues (
+ --- The ID of this value.
+ idTestResultValue INTEGER DEFAULT NEXTVAL('TestResultValueIdSeq'), -- PRIMARY KEY
+ --- The test result it was reported within.
+ idTestResult INTEGER NOT NULL, -- REFERENCES TestResults(idTestResult) NOT NULL,
+ --- The test result it was reported within.
+ idTestSet INTEGER NOT NULL, -- REFERENCES TestSets(idTestSet) NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The name.
+ idStrName INTEGER NOT NULL, -- REFERENCES TestResultStrTab(idStr) NOT NULL,
+ --- The value.
+ lValue bigint NOT NULL,
+ --- The unit.
+ -- @todo This is currently not defined properly. Will fix/correlate this
+ -- with the other places we use unit (IPRT/testdriver/VMMDev).
+ iUnit smallint NOT NULL --CHECK (iUnit >= 0 AND iUnit < 1024)
+);
+COMMIT;
+\d+ NewTestResultValues
+
+-- Note! Using left out join here to speed up things (no hashing).
+SELECT COUNT(*) FROM TestResultValues a LEFT OUTER JOIN TestResults b ON (a.idTestResult = b.idTestResult);
+SELECT COUNT(*) FROM TestResultValues;
+
+INSERT INTO NewTestResultValues (idTestResultValue, idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit)
+ SELECT a.idTestResultValue, a.idTestResult, b.idTestSet, a.tsCreated, a.idStrName, a.lValue, a.iUnit
+ FROM TestResultValues a LEFT OUTER JOIN TestResults b ON (a.idTestResult = b.idTestResult);
+COMMIT;
+SELECT COUNT(*) FROM NewTestResultValues;
+
+-- Switch the tables.
+ALTER TABLE TestResultValues RENAME TO OldTestResultValues;
+ALTER TABLE NewTestResultValues RENAME TO TestResultValues;
+COMMIT;
+
+-- Index the table.
+CREATE INDEX TestResultValuesIdx ON TestResultValues(idTestResult);
+CREATE INDEX TestResultValuesNameIdx ON TestResultValues(idStrName, tsCreated);
+COMMIT;
+
+-- Drop the old table.
+DROP TABLE OldTestResultValues;
+COMMIT;
+
+-- Add the constraints constraint.
+ALTER TABLE TestResultValues ADD CONSTRAINT TestResultValues_iUnit_Check CHECK (iUnit >= 0 AND iUnit < 1024);
+ALTER TABLE TestResultValues ADD PRIMARY KEY (idTestResultValue);
+ALTER TABLE TestResultValues ADD FOREIGN KEY (idStrName) REFERENCES TestResultstrtab(idStr);
+ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult);
+ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet);
+COMMIT;
+
+\d+ TestResultValues;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r11-testsets-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r11-testsets-2.pgsql
new file mode 100644
index 00000000..1df2807f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r11-testsets-2.pgsql
@@ -0,0 +1,214 @@
+-- $Id: tmdb-r11-testsets-2.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds an idBuildCategories to TestSets.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Drop all indexes (might already be dropped).
+--
+DROP INDEX TestSetsGangIdx;
+DROP INDEX TestSetsBoxIdx;
+DROP INDEX TestSetsBuildIdx;
+DROP INDEX TestSetsTestCaseIdx;
+DROP INDEX TestSetsTestVarIdx;
+DROP INDEX TestSetsCreated;
+DROP INDEX TestSetsDone;
+
+--
+-- Drop foreign keys on this table.
+--
+ALTER TABLE SchedQueues DROP CONSTRAINT SchedQueues_idTestSetGangLeader_fkey;
+ALTER TABLE TestBoxStatuses DROP CONSTRAINT TestBoxStatuses_idTestSet_fkey;
+ALTER TABLE TestResults DROP CONSTRAINT idTestSetFk; -- old name
+ALTER TABLE TestResults DROP CONSTRAINT TestResults_idTestSet_fkey;
+ALTER TABLE TestResultValues DROP CONSTRAINT TestResultValues_idTestSet_fkey;
+
+--
+-- Cleanup after failed runs.
+--
+DROP TABLE NewTestSets;
+DROP TABLE OldTestSets;
+
+-- Die on error from now on.
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ TestSets;
+
+--
+-- Create the new version of the table and filling with the content of the old.
+--
+CREATE TABLE NewTestSets (
+ --- The ID of this test set.
+ idTestSet INTEGER DEFAULT NEXTVAL('TestSetIdSeq') NOT NULL, -- PRIMARY KEY
+
+ --- The test config timestamp, used when reading test config.
+ tsConfig TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ --- When this test set was scheduled.
+ -- idGenTestBox is valid at this point.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ --- When this test completed, i.e. testing stopped. This should only be set once.
+ tsDone TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+ --- The current status.
+ enmStatus TestStatus_T DEFAULT 'running'::TestStatus_T NOT NULL,
+
+ --- The build we're testing.
+ -- Non-unique foreign key: Builds(idBuild)
+ idBuild INTEGER NOT NULL,
+ --- The build category of idBuild when the test started.
+ -- This is for speeding up graph data collection, i.e. avoid idBuild
+ -- the WHERE part of the selection.
+ idBuildCategory INTEGER , -- NOT NULL REFERENCES BuildCategories(idBuildCategory)
+ --- The test suite build we're using to do the testing.
+ -- This is NULL if the test suite zip wasn't referred or if a test suite
+ -- build source wasn't configured.
+ -- Non-unique foreign key: Builds(idBuild)
+ idBuildTestSuite INTEGER DEFAULT NULL,
+
+ --- The exact testbox configuration.
+ idGenTestBox INTEGER NOT NULL, -- REFERENCES TestBoxes(idGenTestBox)
+ --- The testbox ID for joining with (valid: tsStarted).
+ -- Non-unique foreign key: TestBoxes(idTestBox)
+ idTestBox INTEGER NOT NULL,
+
+ --- The testgroup (valid: tsConfig).
+ -- Non-unique foreign key: TestBoxes(idTestGroup)
+ -- Note! This also gives the member ship entry, since a testcase can only
+ -- have one membership per test group.
+ idTestGroup INTEGER NOT NULL,
+
+ --- The exact test case config we executed in this test run.
+ idGenTestCase INTEGER NOT NULL, -- REFERENCES TestCases(idGenTestCase)
+ --- The test case ID for joining with (valid: tsConfig).
+ -- Non-unique foreign key: TestBoxes(idTestCase)
+ idTestCase INTEGER NOT NULL,
+
+ --- The arguments (and requirements++) we executed this test case with.
+ idGenTestCaseArgs INTEGER NOT NULL, -- REFERENCES TestCaseArgs(idGenTestCaseArgs)
+ --- The argument variation ID (valid: tsConfig).
+ -- Non-unique foreign key: TestCaseArgs(idTestCaseArgs)
+ idTestCaseArgs INTEGER NOT NULL,
+
+ --- The root of the test result tree.
+ -- @note This will only be NULL early in the transaction setting up the testset.
+ -- @note If the test reports more than one top level test result, we'll
+ -- fail the whole test run and let the test developer fix it.
+ idTestResult INTEGER DEFAULT NULL, -- REFERENCES TestResults(idTestResult)
+
+ --- The base filename used for storing files related to this test set.
+ -- This is a path relative to wherever TM is dumping log files. In order
+ -- to not become a file system test case, we will try not to put too many
+ -- hundred thousand files in a directory. A simple first approach would
+ -- be to just use the current date (tsCreated) like this:
+ -- TM_FILE_DIR/year/month/day/TestSets.idTestSet
+ --
+ -- The primary log file for the test is this name suffixed by '.log'.
+ --
+ -- The files in the testresultfile table gets their full names like this:
+ -- TM_FILE_DIR/sBaseFilename-testresultfile.id-TestResultStrTab(testresultfile.idStrFilename)
+ --
+ -- @remarks We store this explicitly in case we change the directly layout
+ -- at some later point.
+ sBaseFilename text UNIQUE NOT NULL,
+
+ --- The gang member number number, 0 is the leader.
+ iGangMemberNo SMALLINT DEFAULT 0 NOT NULL, --CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024),
+ --- The test set of the gang leader, NULL if no gang involved.
+ -- @note This is set by the gang leader as well, so that we can find all
+ -- gang members by WHERE idTestSetGangLeader = :id.
+ idTestSetGangLeader INTEGER DEFAULT NULL -- REFERENCES TestSets(idTestSet)
+
+);
+COMMIT;
+\d+ NewTestSets
+
+-- Note! Using left out join here to speed up things (no hashing).
+SELECT COUNT(*) FROM TestSets a LEFT OUTER JOIN Builds b ON (a.idBuild = b.idBuild AND b.tsExpire = 'infinity'::TIMESTAMP);
+SELECT COUNT(*) FROM TestSets;
+
+INSERT INTO NewTestSets (idTestSet, tsConfig, tsCreated, tsDone, enmStatus, idBuild, idBuildCategory, idBuildTestSuite,
+ idGenTestBox, idTestBox, idTestGroup, idGenTestCase, idTestCase, idGenTestCaseArgs, idTestCaseArgs,
+ idTestResult, sBaseFilename, iGangMemberNo, idTestSetGangLeader )
+ SELECT a.idTestSet, a.tsConfig, a.tsCreated, tsDone, a.enmStatus, a.idBuild, b.idBuildCategory, a.idBuildTestSuite,
+ a.idGenTestBox, a.idTestBox, a.idTestGroup, a.idGenTestCase, a.idTestCase, a.idGenTestCaseArgs, a.idTestCaseArgs,
+ a.idTestResult, a.sBaseFilename, a.iGangMemberNo, a.idTestSetGangLeader
+ FROM TestSets a LEFT OUTER JOIN Builds b ON (a.idBuild = b.idBuild AND b.tsExpire = 'infinity'::TIMESTAMP);
+COMMIT;
+SELECT COUNT(*) FROM NewTestSets;
+
+-- Note! 2-3 builds are missing from the Builds table, so fudge it.
+UPDATE NewTestSets
+ SET idBuildCategory = 1
+ WHERE idBuildCategory IS NULL;
+
+-- Switch the tables.
+ALTER TABLE TestSets RENAME TO OldTestSets;
+ALTER TABLE NewTestSets RENAME TO TestSets;
+COMMIT;
+
+-- Index the table.
+CREATE INDEX TestSetsGangIdx ON TestSets (idTestSetGangLeader);
+CREATE INDEX TestSetsBoxIdx ON TestSets (idTestBox, idTestResult);
+CREATE INDEX TestSetsBuildIdx ON TestSets (idBuild, idTestResult);
+CREATE INDEX TestSetsTestCaseIdx ON TestSets (idTestCase, idTestResult);
+CREATE INDEX TestSetsTestVarIdx ON TestSets (idTestCaseArgs, idTestResult);
+CREATE INDEX TestSetsCreated ON TestSets (tsCreated);
+CREATE INDEX TestSetsDone ON TestSets (tsDone);
+COMMIT;
+
+-- Drop the old table.
+DROP TABLE OldTestSets;
+COMMIT;
+
+-- Add the constraints constraint.
+ALTER TABLE TestSets ADD CONSTRAINT TestSets_iGangMemberNo_Check CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024);
+ALTER TABLE TestSets ADD PRIMARY KEY (idTestSet);
+ALTER TABLE TestSets ADD FOREIGN KEY (idBuildCategory) REFERENCES BuildCategories(idBuildCategory);
+ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox);
+ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestCase) REFERENCES TestCases(idGenTestCase);
+ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestCaseArgs) REFERENCES TestCaseArgs(idGenTestCaseArgs);
+ALTER TABLE TestSets ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult);
+ALTER TABLE TestSets ADD FOREIGN KEY (idTestSetGangLeader) REFERENCES TestSets(idTestSet);
+COMMIT;
+
+-- Restore foreign keys.
+LOCK TABLE SchedQueues, TestBoxStatuses, TestResults, TestResultValues IN EXCLUSIVE MODE;
+ALTER TABLE SchedQueues ADD FOREIGN KEY (idTestSetGangLeader) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestBoxStatuses ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResults ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+COMMIT;
+
+\d+ TestSets;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r12-testresultvalues-3-testsets-3.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r12-testresultvalues-3-testsets-3.pgsql
new file mode 100644
index 00000000..57f48dea
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r12-testresultvalues-3-testsets-3.pgsql
@@ -0,0 +1,58 @@
+-- $Id: tmdb-r12-testresultvalues-3-testsets-3.pgsql $
+--- @file
+-- VBox Test Manager Database - Graph related optimizations for TestResultValues and TestSets.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ TestResultValues
+
+-- Rename index to better show it's purpose.
+ALTER INDEX TestResultValuesNameIdx RENAME TO TestResultValuesGraphIdx;
+COMMIT;
+
+-- Combine the tsCreated and tsDone indexes.
+DROP INDEX TestSetsCreated;
+DROP INDEX TestSetsDone;
+CREATE INDEX TestSetsCreatedDoneIdx ON TestSets (tsCreated, tsDone);
+COMMIT;
+
+-- Create index for graph.
+CREATE INDEX TestSetsGraphBoxIdx ON TestSets (idTestBox, tsCreated, tsDone, idBuildCategory, idTestCase);
+COMMIT;
+
+\d+ TestResultValues
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql
new file mode 100644
index 00000000..0dfdd8c7
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql
@@ -0,0 +1,134 @@
+-- $Id: tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds an sRepository to Builds and creates a new VcsRepositories table.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Cleanup after failed runs.
+--
+DROP TABLE NewBuildCategories;
+DROP TABLE OldBuildCategories;
+
+--
+-- Drop foreign keys on this table.
+--
+ALTER TABLE Builds DROP CONSTRAINT NewBuilds_idBuildCategory_fkey;
+ALTER TABLE Builds DROP CONSTRAINT Builds_idBuildCategory_fkey;
+ALTER TABLE TestSets DROP CONSTRAINT TestSets_idBuildCategory_fkey;
+
+-- Die on error from now on.
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ BuildCategories;
+
+--
+-- Create the new version of the table and filling with the content of the old.
+--
+CREATE TABLE NewBuildCategories (
+ --- The build type identifier.
+ idBuildCategory INTEGER PRIMARY KEY DEFAULT NEXTVAL('BuildCategoryIdSeq') NOT NULL,
+ --- Product.
+ -- The product name. For instance 'VBox' or 'VBoxTestSuite'.
+ sProduct TEXT NOT NULL,
+ --- The version control repository name.
+ sRepository TEXT NOT NULL,
+ --- The branch name (in the version control system).
+ sBranch TEXT NOT NULL,
+ --- The build type.
+ -- See KBUILD_BLD_TYPES in kBuild for a list of standard build types.
+ sType TEXT NOT NULL,
+ --- Array of the 'sOs.sCpuArch' supported by the build.
+ -- See KBUILD_OSES in kBuild for a list of standard target OSes, and
+ -- KBUILD_ARCHES for a list of standard architectures.
+ --
+ -- @remarks 'os-agnostic' is used if the build doesn't really target any
+ -- specific OS or if it targets all applicable OSes.
+ -- 'noarch' is used if the build is architecture independent or if
+ -- all applicable architectures are handled.
+ -- Thus, 'os-agnostic.noarch' will run on all build boxes.
+ --
+ -- @note The array shall be sorted ascendingly to prevent unnecessary duplicates!
+ --
+ asOsArches TEXT ARRAY NOT NULL,
+
+ UNIQUE (sProduct, sRepository, sBranch, sType, asOsArches)
+);
+COMMIT;
+\d+ NewBuildCategories
+
+INSERT INTO NewBuildCategories (idBuildCategory, sProduct, sRepository, sBranch, sType, asOsArches)
+ SELECT idBuildCategory, sProduct, 'vbox', sBranch, sType, asOsArches
+ FROM BuildCategories
+COMMIT;
+
+-- Switch the tables.
+ALTER TABLE BuildCategories RENAME TO OldBuildCategories;
+ALTER TABLE NewBuildCategories RENAME TO BuildCategories;
+COMMIT;
+
+-- Drop the old table.
+DROP TABLE OldBuildCategories;
+COMMIT;
+
+-- Restore foreign keys.
+LOCK TABLE Builds, TestSets;
+ALTER TABLE Builds ADD FOREIGN KEY (idBuildCategory) REFERENCES BuildCategories(idBuildCategory);
+ALTER TABLE TestSets ADD FOREIGN KEY (idBuildCategory) REFERENCES BuildCategories(idBuildCategory);
+COMMIT;
+
+\d+ BuildCategories;
+
+
+--
+-- Create the new VcsRevisions table.
+--
+CREATE TABLE VcsRevisions (
+ --- The version control tree name.
+ sRepository TEXT NOT NULL,
+ --- The version control tree revision number.
+ iRevision INTEGER NOT NULL,
+ --- When the revision was created (committed).
+ tsCreated TIMESTAMP WITH TIME ZONE NOT NULL,
+ --- The name of the committer.
+ -- @note Not to be confused with uidAuthor and test manager users.
+ sAuthor TEXT,
+ --- The commit message.
+ sMessage TEXT,
+
+ UNIQUE (sRepository, iRevision)
+);
+COMMIT;
+\d+ VcsRevisions;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r14-testboxes-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r14-testboxes-2.pgsql
new file mode 100644
index 00000000..784398b5
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r14-testboxes-2.pgsql
@@ -0,0 +1,201 @@
+-- $Id: tmdb-r14-testboxes-2.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds sCpuName, lCpuRevision and sReport to TestBoxes.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+DROP TABLE OldTestBoxes;
+DROP TABLE NewTestBoxes;
+
+\d TestBoxes;
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE;
+
+DROP INDEX TestBoxesUuidIdx;
+
+--
+-- Rename the original table, drop constrains and foreign key references so we
+-- get the right name automatic when creating the new one.
+--
+ALTER TABLE TestBoxes RENAME TO OldTestBoxes;
+
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_ccpus_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbmemory_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbscratch_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pctscaletimeout_check;
+
+ALTER TABLE TestBoxStatuses DROP CONSTRAINT TestBoxStatuses_idGenTestBox_fkey;
+ALTER TABLE TestSets DROP CONSTRAINT TestSets_idGenTestBox_fkey;
+
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pkey;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_idgentestbox_key;
+
+--
+-- Create the new table, filling it with the current TestBoxes content.
+--
+CREATE TABLE TestBoxes (
+ --- The fixed testbox ID.
+ -- This is assigned when the testbox is created and will never change.
+ idTestBox INTEGER DEFAULT NEXTVAL('TestBoxIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- When modified automatically by the testbox, NULL is used.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER DEFAULT NULL,
+ --- Generation ID for this row.
+ -- This is primarily for referencing by TestSets.
+ idGenTestBox INTEGER UNIQUE DEFAULT NEXTVAL('TestBoxGenIdSeq') NOT NULL,
+
+ --- The testbox IP.
+ -- This is from the webserver point of view and automatically updated on
+ -- SIGNON. The test setup doesn't permit for IP addresses to change while
+ -- the testbox is operational, because this will break gang tests.
+ ip inet NOT NULL,
+ --- The system or firmware UUID.
+ -- This uniquely identifies the testbox when talking to the server. After
+ -- SIGNON though, the testbox will also provide idTestBox and ip to
+ -- establish its identity beyond doubt.
+ uuidSystem uuid NOT NULL,
+ --- The testbox name.
+ -- Usually similar to the DNS name.
+ sName text NOT NULL,
+ --- Optional testbox description.
+ -- Intended for describing the box as well as making other relevant notes.
+ sDescription text DEFAULT NULL,
+
+ --- Reference to the scheduling group that this testbox is a member of.
+ -- Non-unique foreign key: SchedGroups(idSchedGroup)
+ -- A testbox is always part of a group, the default one nothing else.
+ idSchedGroup INTEGER DEFAULT 1 NOT NULL,
+
+ --- Indicates whether this testbox is enabled.
+ -- A testbox gets disabled when we're doing maintenance, debugging a issue
+ -- that happens only on that testbox, or some similar stuff. This is an
+ -- alternative to deleting the testbox.
+ fEnabled BOOLEAN DEFAULT NULL,
+
+ --- The kind of lights-out-management.
+ enmLomKind LomKind_T DEFAULT 'none'::LomKind_T NOT NULL,
+ --- The IP adress of the lights-out-management.
+ -- This can be NULL if enmLomKind is 'none', otherwise it must contain a valid address.
+ ipLom inet DEFAULT NULL,
+
+ --- Timeout scale factor, given as a percent.
+ -- This is a crude adjustment of the test case timeout for slower hardware.
+ pctScaleTimeout smallint DEFAULT 100 NOT NULL CHECK (pctScaleTimeout > 10 AND pctScaleTimeout < 20000),
+
+ --- @name Scheduling properties (reported by testbox script).
+ -- @{
+ --- Same abbrieviations as kBuild, see KBUILD_OSES.
+ sOs text DEFAULT NULL,
+ --- Informational, no fixed format.
+ sOsVersion text DEFAULT NULL,
+ --- Same as CPUID reports (GenuineIntel, AuthenticAMD, CentaurHauls, ...).
+ sCpuVendor text DEFAULT NULL,
+ --- Same as kBuild - x86, amd64, ... See KBUILD_ARCHES.
+ sCpuArch text DEFAULT NULL,
+ --- The CPU name if available.
+ sCpuName text DEFAULT NULL,
+ --- Number identifying the CPU family/model/stepping/whatever.
+ -- For x86 and AMD64 type CPUs, this will on the following format:
+ -- (EffFamily << 24) | (EffModel << 8) | Stepping.
+ lCpuRevision bigint DEFAULT NULL,
+ --- Number of CPUs, CPU cores and CPU threads.
+ cCpus smallint DEFAULT NULL CHECK (cCpus IS NULL OR cCpus > 0),
+ --- Set if capable of hardware virtualization.
+ fCpuHwVirt boolean DEFAULT NULL,
+ --- Set if capable of nested paging.
+ fCpuNestedPaging boolean DEFAULT NULL,
+ --- Set if CPU capable of 64-bit (VBox) guests.
+ fCpu64BitGuest boolean DEFAULT NULL,
+ --- Set if chipset with usable IOMMU (VT-d / AMD-Vi).
+ fChipsetIoMmu boolean DEFAULT NULL,
+ --- The (approximate) memory size in megabytes (rounded down to nearest 4 MB).
+ cMbMemory bigint DEFAULT NULL CHECK (cMbMemory IS NULL OR cMbMemory > 0),
+ --- The amount of scratch space in megabytes (rounded down to nearest 64 MB).
+ cMbScratch bigint DEFAULT NULL CHECK (cMbScratch IS NULL OR cMbScratch >= 0),
+ --- Free form hardware and software report field.
+ sReport text DEFAULT NULL,
+ --- @}
+
+ --- The testbox script revision number, serves the purpose of a version number.
+ -- Probably good to have when scheduling upgrades as well for status purposes.
+ iTestBoxScriptRev INTEGER DEFAULT 0 NOT NULL,
+ --- The python sys.hexversion (layed out as of 2.7).
+ -- Good to know which python versions we need to support.
+ iPythonHexVersion INTEGER DEFAULT NULL,
+
+ --- Pending command.
+ -- @note We put it here instead of in TestBoxStatuses to get history.
+ enmPendingCmd TestBoxCmd_T DEFAULT 'none'::TestBoxCmd_T NOT NULL,
+
+ PRIMARY KEY (idTestBox, tsExpire),
+
+ --- Nested paging requires hardware virtualization.
+ CHECK (fCpuNestedPaging IS NULL OR (fCpuNestedPaging <> TRUE OR fCpuHwVirt = TRUE))
+);
+
+INSERT INTO TestBoxes ( idTestBox, tsEffective, tsExpire, uidAuthor, idGenTestBox, ip, uuidSystem, sName, sDescription,
+ idSchedGroup, fEnabled, enmLomKind, ipLom, pctScaleTimeout, sOs, sOsVersion, sCpuVendor, sCpuArch, sCpuName,
+ lCpuRevision, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest, fChipsetIoMmu, cMbMemory, cMbScratch, sReport,
+ iTestBoxScriptRev, iPythonHexVersion, enmPendingCmd )
+ SELECT idTestBox, tsEffective, tsExpire, uidAuthor, idGenTestBox, ip, uuidSystem, sName, sDescription,
+ idSchedGroup, fEnabled, enmLomKind, ipLom, pctScaleTimeout, sOs, sOsVersion, sCpuVendor, sCpuArch, NULL,
+ NULL, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest, fChipsetIoMmu, cMbMemory, cMbScratch, NULL,
+ iTestBoxScriptRev, iPythonHexVersion, enmPendingCmd
+ FROM OldTestBoxes;
+
+-- Add index.
+CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire);
+
+-- Restore foreign key references to the table.
+ALTER TABLE TestBoxStatuses ADD CONSTRAINT TestBoxStatuses_idGenTestBox_fkey FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox);
+ALTER TABLE TestSets ADD CONSTRAINT TestSets_idGenTestBox_fkey FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox);
+
+-- Drop the old table.
+DROP TABLE OldTestBoxes;
+
+COMMIT;
+
+\d TestBoxes;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r15-index-sorting.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r15-index-sorting.pgsql
new file mode 100644
index 00000000..b95dff80
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r15-index-sorting.pgsql
@@ -0,0 +1,108 @@
+-- $Id: tmdb-r15-index-sorting.pgsql $
+--- @file
+-- VBox Test Manager Database - Index tuning effort.
+--
+
+--
+-- Copyright (C) 2015-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+--
+-- Reordered, modified and new indexes.
+--
+\d UsersLoginNameIdx;
+DROP INDEX UsersLoginNameIdx;
+CREATE INDEX UsersLoginNameIdx ON Users (sLoginName, tsExpire DESC);
+\d UsersLoginNameIdx;
+ANALYZE VERBOSE Users;
+
+
+\d TestCaseArgsLookupIdx;
+DROP INDEX TestCaseArgsLookupIdx;
+CREATE INDEX TestCaseArgsLookupIdx ON TestCaseArgs (idTestCase, tsExpire DESC, tsEffective ASC);
+\d TestCaseArgsLookupIdx;
+ANALYZE VERBOSE TestCaseArgs;
+
+
+\d TestGroups_id_index;
+DROP INDEX TestGroups_id_index;
+CREATE INDEX TestGroups_id_index ON TestGroups (idTestGroup, tsExpire DESC, tsEffective ASC);
+\d TestGroups_id_index;
+ANALYZE VERBOSE TestGroups;
+
+
+\d TestBoxesUuidIdx;
+DROP INDEX TestBoxesUuidIdx;
+CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire DESC);
+\d TestBoxesUuidIdx;
+DROP INDEX IF EXISTS TestBoxesExpireEffectiveIdx;
+CREATE INDEX TestBoxesExpireEffectiveIdx ON TestBoxes (tsExpire DESC, tsEffective ASC);
+\d TestBoxesExpireEffectiveIdx;
+ANALYZE VERBOSE TestBoxes;
+
+
+DROP INDEX IF EXISTS BuildBlacklistIdx;
+CREATE INDEX BuildBlacklistIdx ON BuildBlacklist (iLastRevision DESC, iFirstRevision ASC, sProduct, sBranch,
+ tsExpire DESC, tsEffective ASC);
+\d BuildBlacklist;
+ANALYZE VERBOSE BuildBlacklist;
+
+
+\d TestResultsNameIdx;
+DROP INDEX TestResultsNameIdx;
+CREATE INDEX TestResultsNameIdx ON TestResults (idStrName, tsCreated DESC);
+\d TestResultsNameIdx;
+DROP INDEX IF EXISTS TestResultsNameIdx2;
+CREATE INDEX TestResultsNameIdx2 ON TestResults (idTestResult, idStrName);
+\d TestResultsNameIdx2;
+ANALYZE VERBOSE TestResults;
+
+
+\d TestSetsCreatedDoneIdx;
+DROP INDEX TestSetsCreatedDoneIdx;
+DROP INDEX IF EXISTS TestSetsDoneCreatedBuildCatIdx;
+CREATE INDEX TestSetsDoneCreatedBuildCatIdx ON TestSets (tsDone DESC NULLS FIRST, tsCreated ASC, idBuildCategory);
+\d TestSetsDoneCreatedBuildCatIdx;
+\d TestSetsGraphBoxIdx;
+DROP INDEX TestSetsGraphBoxIdx;
+CREATE INDEX TestSetsGraphBoxIdx ON TestSets (idTestBox, tsCreated DESC, tsDone ASC NULLS LAST, idBuildCategory, idTestCase);
+\d TestSetsGraphBoxIdx;
+ANALYZE VERBOSE TestSets;
+
+
+DROP INDEX IF EXISTS SchedQueuesItemIdx;
+CREATE INDEX SchedQueuesItemIdx ON SchedQueues(idItem);
+\d SchedQueuesItemIdx;
+DROP INDEX IF EXISTS SchedQueuesSchedGroupIdx;
+CREATE INDEX SchedQueuesSchedGroupIdx ON SchedQueues(idSchedGroup);
+\d SchedQueuesSchedGroupIdx;
+ANALYZE VERBOSE SchedQueues;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql
new file mode 100644
index 00000000..108da811
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql
@@ -0,0 +1,122 @@
+-- $Id: tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds sName to TestCaseArgs, idTestSet
+-- to TestResultFailures and add some indexes to the latter as well.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+DROP TABLE OldTestCaseArgs;
+DROP TABLE NewTestCaseArgs;
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestCaseArgs IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestResultFailures IN ACCESS EXCLUSIVE MODE;
+
+--
+-- TestCaseArgs is simple and we can use ALTER TABLE for a change.
+--
+\d TestCaseArgs;
+ALTER TABLE TestCaseArgs ADD COLUMN sSubName text DEFAULT NULL;
+\d TestCaseArgs;
+
+
+--
+-- Rename the original table, drop constrains and foreign key references so we
+-- get the right name automatic when creating the new one.
+--
+\d TestResultFailures;
+ALTER TABLE TestResultFailures DROP CONSTRAINT idTestResultFk;
+ALTER TABLE TestResultFailures RENAME TO OldTestResultFailures;
+
+DROP INDEX IF EXISTS TestResultFailureIdx;
+DROP INDEX IF EXISTS TestResultFailureIdx2;
+DROP INDEX IF EXISTS TestResultFailureIdx3;
+
+
+CREATE TABLE TestResultFailures (
+ --- The test result we're disucssing.
+ -- @note The foreign key is declared after TestResults (further down).
+ idTestResult INTEGER NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+ --- The testsest this result is a part of.
+ -- This is mainly an aid for bypassing the enormous TestResults table.
+ -- Note! This is a foreign key, but we have to add it after TestSets has
+ -- been created, see further down.
+ idTestSet INTEGER NOT NULL,
+
+ --- The suggested failure reason.
+ -- Non-unique foreign key: FailureReasons(idFailureReason)
+ idFailureReason INTEGER NOT NULL,
+ --- Optional comment.
+ sComment text DEFAULT NULL,
+
+ PRIMARY KEY (idTestResult, tsExpire)
+);
+
+INSERT INTO TestResultFailures ( idTestResult, tsEffective, tsExpire, uidAuthor, idTestSet, idFailureReason, sComment )
+ SELECT o.idTestResult, o.tsEffective, o.tsExpire, o.uidAuthor, tr.idTestSet, o.idFailureReason, sComment
+ FROM OldTestResultFailures o,
+ TestResults tr
+ WHERE o.idTestResult = tr.idTestResult;
+
+-- Add unique constraint to TestResult for our new foreign key.
+ALTER TABLE TestResults ADD CONSTRAINT TestResults_idTestResult_idTestSet_key UNIQUE (idTestResult, idTestSet);
+
+-- Restore foreign key.
+ALTER TABLE TestResultFailures ADD CONSTRAINT TestResultFailures_idTestResult_idTestSet_fkey
+ FOREIGN KEY (idTestResult, idTestSet) REFERENCES TestResults(idTestResult, idTestSet) MATCH FULL;
+
+-- Add new indexes.
+CREATE INDEX TestResultFailureIdx ON TestResultFailures (idTestSet, tsExpire DESC, tsEffective ASC);
+CREATE INDEX TestResultFailureIdx2 ON TestResultFailures (idTestResult, tsExpire DESC, tsEffective ASC);
+CREATE INDEX TestResultFailureIdx3 ON TestResultFailures (idFailureReason, idTestResult, tsExpire DESC, tsEffective ASC);
+
+-- Drop the old table.
+DROP TABLE OldTestResultFailures;
+
+COMMIT;
+
+\d TestResultFailures;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r17-testresultvalues-4.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r17-testresultvalues-4.pgsql
new file mode 100644
index 00000000..d97954b6
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r17-testresultvalues-4.pgsql
@@ -0,0 +1,48 @@
+-- $Id: tmdb-r17-testresultvalues-4.pgsql $
+--- @file
+-- VBox Test Manager Database - Log viewer related optimizations for TestResultValues.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+\d+ TestResultValues
+
+-- Create index for the log viewer
+CREATE INDEX TestResultValuesLogIdx ON TestResultValues(idTestSet, tsCreated);
+COMMIT;
+
+\d+ TestResultValues
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql
new file mode 100644
index 00000000..88788273
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql
@@ -0,0 +1,171 @@
+-- $Id: tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds an idTestSet to TestResultFiles and TestResultMsgs.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Cleanup after failed runs.
+--
+DROP TABLE IF EXISTS NewTestResultFiles;
+DROP TABLE IF EXISTS OldTestResultFiles;
+DROP TABLE IF EXISTS NewTestResultMsgs;
+DROP TABLE IF EXISTS OldTestResultMsgs;
+
+-- Die on error from now on.
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+
+--
+-- Rename the original table, drop constrains and foreign key references so we
+-- get the right name automatic when creating the new one.
+--
+\d+ TestResultFiles;
+ALTER TABLE TestResultFiles RENAME TO OldTestResultFiles;
+
+DROP INDEX IF EXISTS TestResultFilesIdx;
+DROP INDEX IF EXISTS TestResultFilesIdx2;
+
+--
+-- Create the new version of the table and filling with the content of the old.
+--
+CREATE TABLE TestResultFiles (
+ --- The ID of this file.
+ idTestResultFile INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultFileId'),
+ --- The test result it was reported within.
+ idTestResult INTEGER NOT NULL,
+ --- The test set this file is a part of (for avoiding joining thru TestResults).
+ idTestSet INTEGER NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The filename relative to TestSets(sBaseFilename) + '-'.
+ -- The set of valid filename characters should be very limited so that no
+ -- file system issues can occure either on the TM side or the user when
+ -- loading the files. Tests trying to use other characters will fail.
+ -- Valid character regular expession: '^[a-zA-Z0-9_-(){}#@+,.=]*$'
+ idStrFile INTEGER NOT NULL,
+ --- The description.
+ idStrDescription INTEGER NOT NULL,
+ --- The kind of file.
+ -- For instance: 'log/release/vm',
+ -- 'screenshot/failure',
+ -- 'screencapture/failure',
+ -- 'xmllog/somestuff'
+ idStrKind INTEGER NOT NULL,
+ --- The mime type for the file.
+ -- For instance: 'text/plain',
+ -- 'image/png',
+ -- 'video/webm',
+ -- 'text/xml'
+ idStrMime INTEGER NOT NULL
+);
+
+INSERT INTO TestResultFiles ( idTestResultFile, idTestResult, idTestSet, tsCreated, idStrFile, idStrDescription,
+ idStrKind, idStrMime)
+ SELECT o.idTestResultFile, o.idTestResult, tr.idTestSet, o.tsCreated, o.idStrFile, o.idStrDescription,
+ o.idStrKind, o.idStrMime
+ FROM OldTestResultFiles o,
+ TestResults tr
+ WHERE o.idTestResult = tr.idTestResult;
+
+-- Add new indexes.
+CREATE INDEX TestResultFilesIdx ON TestResultFiles(idTestResult);
+CREATE INDEX TestResultFilesIdx2 ON TestResultFiles(idTestSet, tsCreated DESC);
+
+-- Restore foreign keys.
+ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idTestResult_fkey FOREIGN KEY(idTestResult) REFERENCES TestResults(idTestResult);
+ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idTestSet_fkey FOREIGN KEY(idTestSet) REFERENCES TestSets(idTestSet);
+ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idStrFile_fkey FOREIGN KEY(idStrFile) REFERENCES TestResultStrTab(idStr);
+ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idStrDescription_fkey FOREIGN KEY(idStrDescription) REFERENCES TestResultStrTab(idStr);
+ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idStrKind_fkey FOREIGN KEY(idStrKind) REFERENCES TestResultStrTab(idStr);
+ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idStrMime_fkey FOREIGN KEY(idStrMime) REFERENCES TestResultStrTab(idStr);
+
+\d TestResultFiles;
+
+
+--
+-- Rename the original table, drop constrains and foreign key references so we
+-- get the right name automatic when creating the new one.
+--
+\d+ TestResultMsgs;
+ALTER TABLE TestResultMsgs RENAME TO OldTestResultMsgs;
+
+DROP INDEX IF EXISTS TestResultMsgsIdx;
+DROP INDEX IF EXISTS TestResultMsgsIdx2;
+
+--
+-- Create the new version of the table and filling with the content of the old.
+--
+CREATE TABLE TestResultMsgs (
+ --- The ID of this file.
+ idTestResultMsg INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultMsgIdSeq'),
+ --- The test result it was reported within.
+ idTestResult INTEGER NOT NULL,
+ --- The test set this file is a part of (for avoiding joining thru TestResults).
+ idTestSet INTEGER NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- The message string.
+ idStrMsg INTEGER NOT NULL,
+ --- The message level.
+ enmLevel TestResultMsgLevel_T NOT NULL
+);
+
+INSERT INTO TestResultMsgs ( idTestResultMsg, idTestResult, idTestSet, tsCreated, idStrMsg, enmLevel)
+ SELECT o.idTestResultMsg, o.idTestResult, tr.idTestSet, o.tsCreated, o.idStrMsg, o.enmLevel
+ FROM OldTestResultMsgs o,
+ TestResults tr
+ WHERE o.idTestResult = tr.idTestResult;
+
+-- Add new indexes.
+CREATE INDEX TestResultMsgsIdx ON TestResultMsgs(idTestResult);
+CREATE INDEX TestResultMsgsIdx2 ON TestResultMsgs(idTestSet, tsCreated DESC);
+
+-- Restore foreign keys.
+ALTER TABLE TestResultMsgs ADD CONSTRAINT TestResultMsgs_idTestResult_fkey FOREIGN KEY(idTestResult) REFERENCES TestResults(idTestResult);
+ALTER TABLE TestResultMsgs ADD CONSTRAINT TestResultMsgs_idTestSet_fkey FOREIGN KEY(idTestSet) REFERENCES TestSets(idTestSet);
+ALTER TABLE TestResultMsgs ADD CONSTRAINT TestResultMsgs_idStrMsg_fkey FOREIGN KEY(idStrMsg) REFERENCES TestResultStrTab(idStr);
+
+
+\d TestResultMsgs;
+
+
+--
+-- Drop the old tables and commit.
+--
+DROP TABLE OldTestResultFiles;
+DROP TABLE OldTestResultMsgs;
+
+COMMIT;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r19-testboxes-3.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r19-testboxes-3.pgsql
new file mode 100644
index 00000000..26228a9d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r19-testboxes-3.pgsql
@@ -0,0 +1,356 @@
+-- $Id: tmdb-r19-testboxes-3.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds sComment and fRawMode to TestBoxes and
+-- moves the strings to separate table.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Cleanup after failed runs.
+--
+DROP TABLE IF EXISTS OldTestBoxes;
+
+-- Die on error from now on.
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+-- Sanity check that we haven't already run this script.
+SELECT 'done conversion already?', COUNT(sReport) FROM TestBoxes WHERE tsExpire = 'infinity'::TIMESTAMP;
+
+-- Total grid lock.
+LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE SchedGroupMembers IN ACCESS EXCLUSIVE MODE;
+
+\d+ TestBoxes;
+
+--
+-- Rename the table, drop foreign keys refering to it, and drop constrains
+-- within the table itself. The latter is mostly for naming and we do it
+-- up front in case the database we're running against has different names
+-- due to previous conversions.
+--
+ALTER TABLE TestBoxes RENAME TO OldTestBoxes;
+
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_ccpus_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbmemory_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbscratch_check;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pctscaletimeout_check;
+
+ALTER TABLE TestBoxStatuses DROP CONSTRAINT TestBoxStatuses_idGenTestBox_fkey;
+ALTER TABLE TestSets DROP CONSTRAINT TestSets_idGenTestBox_fkey;
+
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pkey;
+ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_idgentestbox_key;
+
+DROP INDEX IF EXISTS TestBoxesUuidIdx;
+DROP INDEX IF EXISTS TestBoxesExpireEffectiveIdx;
+
+-- This output should be free of index, constraints and references from other tables.
+\d+ OldTestBoxes;
+
+--
+-- Create the two new tables before starting data migration (don't want to spend time
+-- on converting strings just to find a typo in the TestBoxes create table syntax).
+--
+CREATE SEQUENCE TestBoxStrTabIdSeq
+ START 1
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+CREATE TABLE TestBoxStrTab (
+ --- The ID of this string.
+ idStr INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestBoxStrTabIdSeq'),
+ --- The string value.
+ sValue text NOT NULL,
+ --- Creation time stamp.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL
+);
+
+CREATE TABLE TestBoxes (
+ --- The fixed testbox ID.
+ -- This is assigned when the testbox is created and will never change.
+ idTestBox INTEGER DEFAULT NEXTVAL('TestBoxIdSeq') NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- When modified automatically by the testbox, NULL is used.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER DEFAULT NULL,
+ --- Generation ID for this row.
+ -- This is primarily for referencing by TestSets.
+ idGenTestBox INTEGER UNIQUE DEFAULT NEXTVAL('TestBoxGenIdSeq') NOT NULL,
+
+ --- The testbox IP.
+ -- This is from the webserver point of view and automatically updated on
+ -- SIGNON. The test setup doesn't permit for IP addresses to change while
+ -- the testbox is operational, because this will break gang tests.
+ ip inet NOT NULL,
+ --- The system or firmware UUID.
+ -- This uniquely identifies the testbox when talking to the server. After
+ -- SIGNON though, the testbox will also provide idTestBox and ip to
+ -- establish its identity beyond doubt.
+ uuidSystem uuid NOT NULL,
+ --- The testbox name.
+ -- Usually similar to the DNS name.
+ sName text NOT NULL,
+ --- Optional testbox description.
+ -- Intended for describing the box as well as making other relevant notes.
+ idStrDescription INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+
+ --- Reference to the scheduling group that this testbox is a member of.
+ -- Non-unique foreign key: SchedGroups(idSchedGroup)
+ -- A testbox is always part of a group, the default one nothing else.
+ idSchedGroup INTEGER DEFAULT 1 NOT NULL,
+
+ --- Indicates whether this testbox is enabled.
+ -- A testbox gets disabled when we're doing maintenance, debugging a issue
+ -- that happens only on that testbox, or some similar stuff. This is an
+ -- alternative to deleting the testbox.
+ fEnabled BOOLEAN DEFAULT NULL,
+
+ --- The kind of lights-out-management.
+ enmLomKind LomKind_T DEFAULT 'none'::LomKind_T NOT NULL,
+ --- The IP adress of the lights-out-management.
+ -- This can be NULL if enmLomKind is 'none', otherwise it must contain a valid address.
+ ipLom inet DEFAULT NULL,
+
+ --- Timeout scale factor, given as a percent.
+ -- This is a crude adjustment of the test case timeout for slower hardware.
+ pctScaleTimeout smallint DEFAULT 100 NOT NULL CHECK (pctScaleTimeout > 10 AND pctScaleTimeout < 20000),
+
+ --- Change comment or similar.
+ idStrComment INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+
+ --- @name Scheduling properties (reported by testbox script).
+ -- @{
+ --- Same abbrieviations as kBuild, see KBUILD_OSES.
+ idStrOs INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- Informational, no fixed format.
+ idStrOsVersion INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- Same as CPUID reports (GenuineIntel, AuthenticAMD, CentaurHauls, ...).
+ idStrCpuVendor INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- Same as kBuild - x86, amd64, ... See KBUILD_ARCHES.
+ idStrCpuArch INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- The CPU name if available.
+ idStrCpuName INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- Number identifying the CPU family/model/stepping/whatever.
+ -- For x86 and AMD64 type CPUs, this will on the following format:
+ -- (EffFamily << 24) | (EffModel << 8) | Stepping.
+ lCpuRevision bigint DEFAULT NULL,
+ --- Number of CPUs, CPU cores and CPU threads.
+ cCpus smallint DEFAULT NULL CHECK (cCpus IS NULL OR cCpus > 0),
+ --- Set if capable of hardware virtualization.
+ fCpuHwVirt boolean DEFAULT NULL,
+ --- Set if capable of nested paging.
+ fCpuNestedPaging boolean DEFAULT NULL,
+ --- Set if CPU capable of 64-bit (VBox) guests.
+ fCpu64BitGuest boolean DEFAULT NULL,
+ --- Set if chipset with usable IOMMU (VT-d / AMD-Vi).
+ fChipsetIoMmu boolean DEFAULT NULL,
+ --- Set if the test box does raw-mode tests.
+ fRawMode boolean DEFAULT NULL,
+ --- The (approximate) memory size in megabytes (rounded down to nearest 4 MB).
+ cMbMemory bigint DEFAULT NULL CHECK (cMbMemory IS NULL OR cMbMemory > 0),
+ --- The amount of scratch space in megabytes (rounded down to nearest 64 MB).
+ cMbScratch bigint DEFAULT NULL CHECK (cMbScratch IS NULL OR cMbScratch >= 0),
+ --- Free form hardware and software report field.
+ idStrReport INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL,
+ --- @}
+
+ --- The testbox script revision number, serves the purpose of a version number.
+ -- Probably good to have when scheduling upgrades as well for status purposes.
+ iTestBoxScriptRev INTEGER DEFAULT 0 NOT NULL,
+ --- The python sys.hexversion (layed out as of 2.7).
+ -- Good to know which python versions we need to support.
+ iPythonHexVersion INTEGER DEFAULT NULL,
+
+ --- Pending command.
+ -- @note We put it here instead of in TestBoxStatuses to get history.
+ enmPendingCmd TestBoxCmd_T DEFAULT 'none'::TestBoxCmd_T NOT NULL,
+
+ PRIMARY KEY (idTestBox, tsExpire),
+
+ --- Nested paging requires hardware virtualization.
+ CHECK (fCpuNestedPaging IS NULL OR (fCpuNestedPaging <> TRUE OR fCpuHwVirt = TRUE))
+);
+
+-- Convenience view that simplifies querying a lot.
+CREATE VIEW TestBoxesWithStrings AS
+ SELECT TestBoxes.*,
+ Str1.sValue AS sDescription,
+ Str2.sValue AS sComment,
+ Str3.sValue AS sOs,
+ Str4.sValue AS sOsVersion,
+ Str5.sValue AS sCpuVendor,
+ Str6.sValue AS sCpuArch,
+ Str7.sValue AS sCpuName,
+ Str8.sValue AS sReport
+ FROM TestBoxes
+ LEFT OUTER JOIN TestBoxStrTab Str1 ON idStrDescription = Str1.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str2 ON idStrComment = Str2.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str3 ON idStrOs = Str3.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str4 ON idStrOsVersion = Str4.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str5 ON idStrCpuVendor = Str5.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str6 ON idStrCpuArch = Str6.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str7 ON idStrCpuName = Str7.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str8 ON idStrReport = Str8.idStr;
+
+
+--
+-- Populate the string table.
+--
+
+--- Empty string with ID 0.
+INSERT INTO TestBoxStrTab (idStr, sValue) VALUES (0, '');
+
+INSERT INTO TestBoxStrTab (sValue)
+( SELECT DISTINCT sDescription FROM OldTestBoxes WHERE sDescription IS NOT NULL
+) UNION ( SELECT DISTINCT sOs FROM OldTestBoxes WHERE sOs IS NOT NULL
+) UNION ( SELECT DISTINCT sOsVersion FROM OldTestBoxes WHERE sOsVersion IS NOT NULL
+) UNION ( SELECT DISTINCT sCpuVendor FROM OldTestBoxes WHERE sCpuVendor IS NOT NULL
+) UNION ( SELECT DISTINCT sCpuArch FROM OldTestBoxes WHERE sCpuArch IS NOT NULL
+) UNION ( SELECT DISTINCT sCpuName FROM OldTestBoxes WHERE sCpuName IS NOT NULL
+) UNION ( SELECT DISTINCT sReport FROM OldTestBoxes WHERE sReport IS NOT NULL );
+
+-- Index and analyze the string table as we'll be using it a lot below already.
+CREATE INDEX TestBoxStrTabNameIdx ON TestBoxStrTab USING hash (sValue);
+ANALYZE VERBOSE TestBoxStrTab;
+
+SELECT MAX(idStr) FROM TestBoxStrTab;
+SELECT pg_total_relation_size('TestBoxStrTab');
+
+
+--
+-- Populate the test box table.
+--
+
+INSERT INTO TestBoxes (
+ idTestBox, -- 0
+ tsEffective, -- 1
+ tsExpire, -- 2
+ uidAuthor, -- 3
+ idGenTestBox, -- 4
+ ip, -- 5
+ uuidSystem, -- 6
+ sName, -- 7
+ idStrDescription, -- 8
+ idSchedGroup, -- 9
+ fEnabled, -- 10
+ enmLomKind, -- 11
+ ipLom, -- 12
+ pctScaleTimeout, -- 13
+ idStrComment, -- 14
+ idStrOs, -- 15
+ idStrOsVersion, -- 16
+ idStrCpuVendor, -- 17
+ idStrCpuArch, -- 18
+ idStrCpuName, -- 19
+ lCpuRevision, -- 20
+ cCpus, -- 21
+ fCpuHwVirt, -- 22
+ fCpuNestedPaging, -- 23
+ fCpu64BitGuest, -- 24
+ fChipsetIoMmu, -- 25
+ fRawMode, -- 26
+ cMbMemory, -- 27
+ cMbScratch, -- 28
+ idStrReport, -- 29
+ iTestBoxScriptRev, -- 30
+ iPythonHexVersion, -- 31
+ enmPendingCmd -- 32
+ )
+SELECT idTestBox,
+ tsEffective,
+ tsExpire,
+ uidAuthor,
+ idGenTestBox,
+ ip,
+ uuidSystem,
+ sName,
+ st1.idStr,
+ idSchedGroup,
+ fEnabled,
+ enmLomKind,
+ ipLom,
+ pctScaleTimeout,
+ NULL,
+ st2.idStr,
+ st3.idStr,
+ st4.idStr,
+ st5.idStr,
+ st6.idStr,
+ lCpuRevision,
+ cCpus,
+ fCpuHwVirt,
+ fCpuNestedPaging,
+ fCpu64BitGuest,
+ fChipsetIoMmu,
+ NULL,
+ cMbMemory,
+ cMbScratch,
+ st7.idStr,
+ iTestBoxScriptRev,
+ iPythonHexVersion,
+ enmPendingCmd
+FROM OldTestBoxes
+ LEFT OUTER JOIN TestBoxStrTab st1 ON sDescription = st1.sValue
+ LEFT OUTER JOIN TestBoxStrTab st2 ON sOs = st2.sValue
+ LEFT OUTER JOIN TestBoxStrTab st3 ON sOsVersion = st3.sValue
+ LEFT OUTER JOIN TestBoxStrTab st4 ON sCpuVendor = st4.sValue
+ LEFT OUTER JOIN TestBoxStrTab st5 ON sCpuArch = st5.sValue
+ LEFT OUTER JOIN TestBoxStrTab st6 ON sCpuName = st6.sValue
+ LEFT OUTER JOIN TestBoxStrTab st7 ON sReport = st7.sValue;
+
+-- Restore indexes.
+CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire DESC);
+CREATE INDEX TestBoxesExpireEffectiveIdx ON TestBoxes (tsExpire DESC, tsEffective ASC);
+
+-- Restore foreign key references to the table.
+ALTER TABLE TestBoxStatuses ADD CONSTRAINT TestBoxStatuses_idGenTestBox_fkey
+ FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox);
+ALTER TABLE TestSets ADD CONSTRAINT TestSets_idGenTestBox_fkey
+ FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox);
+
+-- Drop the old table.
+DROP TABLE OldTestBoxes;
+
+COMMIT;
+
+\d TestBoxes;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql
new file mode 100644
index 00000000..ac40ebb8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql
@@ -0,0 +1,67 @@
+-- $Id: tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds sComment to TestCases, TestGroups
+-- and SchedGroups.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestCases IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestGroups IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE SchedGroups IN ACCESS EXCLUSIVE MODE;
+
+--
+-- All the changes are rather simple and we'll just add the sComment column last.
+--
+\d TestCases;
+\d TestGroups;
+\d SchedGroups;
+
+ALTER TABLE TestCases ADD COLUMN sComment TEXT DEFAULT NULL;
+ALTER TABLE TestGroups ADD COLUMN sComment TEXT DEFAULT NULL;
+ALTER TABLE SchedGroups ADD COLUMN sComment TEXT DEFAULT NULL;
+
+\d TestCases;
+\d TestGroups;
+\d SchedGroups;
+
+\prompt "Update python files while everything is locked. Hurry!" dummy
+
+COMMIT;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r21-testsets-4.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r21-testsets-4.pgsql
new file mode 100644
index 00000000..13a57854
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r21-testsets-4.pgsql
@@ -0,0 +1,290 @@
+-- $Id: tmdb-r21-testsets-4.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds an idSchedGroup to TestSets in
+-- preparation for testboxes belonging to multiple scheduling queues.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Cleanup after failed runs.
+--
+DROP TABLE IF EXISTS OldTestSets;
+
+--
+-- Die on error from now on.
+--
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+
+-- Total grid lock (don't want to deadlock below).
+LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestResults IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestResultFailures IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestResultFiles IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestResultMsgs IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestResultValues IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE SchedGroups IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE SchedQueues IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE SchedGroupMembers IN ACCESS EXCLUSIVE MODE;
+
+\d+ TestSets;
+
+--
+-- Rename the table, drop foreign keys refering to it, and drop constrains
+-- within the table itself. The latter is mostly for naming and we do it
+-- up front in case the database we're running against has different names
+-- due to previous conversions.
+--
+ALTER TABLE TestSets RENAME TO OldTestSets;
+
+ALTER TABLE TestResultFailures DROP CONSTRAINT IF EXISTS idtestsetfk;
+ALTER TABLE TestResultFailures DROP CONSTRAINT IF EXISTS TestResultFailures_idTestSet_fkey;
+ALTER TABLE SchedQueues DROP CONSTRAINT IF EXISTS SchedQueues_idTestSetGangLeader_fkey;
+ALTER TABLE TestBoxStatuses DROP CONSTRAINT IF EXISTS TestBoxStatuses_idTestSet_fkey;
+ALTER TABLE TestResultFiles DROP CONSTRAINT IF EXISTS TestResultFiles_idTestSet_fkey;
+ALTER TABLE TestResultMsgs DROP CONSTRAINT IF EXISTS TestResultMsgs_idTestSet_fkey;
+ALTER TABLE TestResults DROP CONSTRAINT IF EXISTS TestResults_idTestSet_fkey;
+ALTER TABLE TestResultValues DROP CONSTRAINT IF EXISTS TestResultValues_idTestSet_fkey;
+ALTER TABLE TestResultValues DROP CONSTRAINT IF EXISTS TestResultValues_idTestSet_fkey1;
+
+ALTER TABLE OldTestSets DROP CONSTRAINT testsets_igangmemberno_check;
+
+ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idBuildCategory_fkey;
+ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idGenTestBox_fkey;
+ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idGenTestCase_fkey;
+ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idGenTestCaseArgs_fkey;
+ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idTestResult_fkey;
+ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idTestSetGangLeader_fkey;
+
+ALTER TABLE OldTestSets DROP CONSTRAINT IF EXISTS TestSets_sBaseFilename_key;
+ALTER TABLE OldTestSets DROP CONSTRAINT IF EXISTS NewTestSets_sBaseFilename_key;
+ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_pkey;
+
+DROP INDEX IF EXISTS TestSetsGangIdx;
+DROP INDEX IF EXISTS TestSetsBoxIdx;
+DROP INDEX IF EXISTS TestSetsBuildIdx;
+DROP INDEX IF EXISTS TestSetsTestCaseIdx;
+DROP INDEX IF EXISTS TestSetsTestVarIdx;
+DROP INDEX IF EXISTS TestSetsDoneCreatedBuildCatIdx;
+DROP INDEX IF EXISTS TestSetsGraphBoxIdx;
+
+
+-- This output should be free of indexes, constraints and references from other tables.
+\d+ OldTestSets;
+
+\prompt "Is the above table completely free of indexes, constraints and references? Ctrl-C if not." dummy
+
+--
+-- Create the new table (no foreign keys).
+--
+CREATE TABLE TestSets (
+ --- The ID of this test set.
+ idTestSet INTEGER DEFAULT NEXTVAL('TestSetIdSeq') NOT NULL,
+
+ --- The test config timestamp, used when reading test config.
+ tsConfig TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ --- When this test set was scheduled.
+ -- idGenTestBox is valid at this point.
+ tsCreated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ --- When this test completed, i.e. testing stopped. This should only be set once.
+ tsDone TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+ --- The current status.
+ enmStatus TestStatus_T DEFAULT 'running'::TestStatus_T NOT NULL,
+
+ --- The build we're testing.
+ -- Non-unique foreign key: Builds(idBuild)
+ idBuild INTEGER NOT NULL,
+ --- The build category of idBuild when the test started.
+ -- This is for speeding up graph data collection, i.e. avoid idBuild
+ -- the WHERE part of the selection.
+ idBuildCategory INTEGER NOT NULL,
+ --- The test suite build we're using to do the testing.
+ -- This is NULL if the test suite zip wasn't referred or if a test suite
+ -- build source wasn't configured.
+ -- Non-unique foreign key: Builds(idBuild)
+ idBuildTestSuite INTEGER DEFAULT NULL,
+
+ --- The exact testbox configuration.
+ idGenTestBox INTEGER NOT NULL,
+ --- The testbox ID for joining with (valid: tsStarted).
+ -- Non-unique foreign key: TestBoxes(idTestBox)
+ idTestBox INTEGER NOT NULL,
+ --- The scheduling group ID the test was scheduled thru (valid: tsStarted).
+ -- Non-unique foreign key: SchedGroups(idSchedGroup)
+ idSchedGroup INTEGER NOT NULL,
+
+ --- The testgroup (valid: tsConfig).
+ -- Non-unique foreign key: TestBoxes(idTestGroup)
+ -- Note! This also gives the member ship entry, since a testcase can only
+ -- have one membership per test group.
+ idTestGroup INTEGER NOT NULL,
+
+ --- The exact test case config we executed in this test run.
+ idGenTestCase INTEGER NOT NULL,
+ --- The test case ID for joining with (valid: tsConfig).
+ -- Non-unique foreign key: TestBoxes(idTestCase)
+ idTestCase INTEGER NOT NULL,
+
+ --- The arguments (and requirements++) we executed this test case with.
+ idGenTestCaseArgs INTEGER NOT NULL,
+ --- The argument variation ID (valid: tsConfig).
+ -- Non-unique foreign key: TestCaseArgs(idTestCaseArgs)
+ idTestCaseArgs INTEGER NOT NULL,
+
+ --- The root of the test result tree.
+ -- @note This will only be NULL early in the transaction setting up the testset.
+ -- @note If the test reports more than one top level test result, we'll
+ -- fail the whole test run and let the test developer fix it.
+ idTestResult INTEGER DEFAULT NULL,
+
+ --- The base filename used for storing files related to this test set.
+ -- This is a path relative to wherever TM is dumping log files. In order
+ -- to not become a file system test case, we will try not to put too many
+ -- hundred thousand files in a directory. A simple first approach would
+ -- be to just use the current date (tsCreated) like this:
+ -- TM_FILE_DIR/year/month/day/TestSets.idTestSet
+ --
+ -- The primary log file for the test is this name suffixed by '.log'.
+ --
+ -- The files in the testresultfile table gets their full names like this:
+ -- TM_FILE_DIR/sBaseFilename-testresultfile.id-TestResultStrTab(testresultfile.idStrFilename)
+ --
+ -- @remarks We store this explicitly in case we change the directly layout
+ -- at some later point.
+ sBaseFilename text NOT NULL,
+
+ --- The gang member number number, 0 is the leader.
+ iGangMemberNo SMALLINT DEFAULT 0 NOT NULL, -- CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024),
+ --- The test set of the gang leader, NULL if no gang involved.
+ -- @note This is set by the gang leader as well, so that we can find all
+ -- gang members by WHERE idTestSetGangLeader = :id.
+ idTestSetGangLeader INTEGER DEFAULT NULL
+
+);
+
+-- Convert the data.
+INSERT INTO TestSets (
+ idTestSet,
+ tsConfig,
+ tsCreated,
+ tsDone,
+ enmStatus,
+ idBuild,
+ idBuildCategory,
+ idBuildTestSuite,
+ idGenTestBox,
+ idTestBox,
+ idSchedGroup,
+ idTestGroup,
+ idGenTestCase,
+ idTestCase,
+ idGenTestCaseArgs,
+ idTestCaseArgs,
+ idTestResult,
+ sBaseFilename,
+ iGangMemberNo,
+ idTestSetGangLeader
+ )
+SELECT OldTestSets.idTestSet,
+ OldTestSets.tsConfig,
+ OldTestSets.tsCreated,
+ OldTestSets.tsDone,
+ OldTestSets.enmStatus,
+ OldTestSets.idBuild,
+ OldTestSets.idBuildCategory,
+ OldTestSets.idBuildTestSuite,
+ OldTestSets.idGenTestBox,
+ OldTestSets.idTestBox,
+ TestBoxes.idSchedGroup,
+ OldTestSets.idTestGroup,
+ OldTestSets.idGenTestCase,
+ OldTestSets.idTestCase,
+ OldTestSets.idGenTestCaseArgs,
+ OldTestSets.idTestCaseArgs,
+ OldTestSets.idTestResult,
+ OldTestSets.sBaseFilename,
+ OldTestSets.iGangMemberNo,
+ OldTestSets.idTestSetGangLeader
+FROM OldTestSets
+ INNER JOIN TestBoxes
+ ON OldTestSets.idGenTestBox = TestBoxes.idGenTestBox;
+
+-- Restore the primary key and unique constraints.
+ALTER TABLE TestSets ADD PRIMARY KEY (idTestSet);
+ALTER TABLE TestSets ADD UNIQUE (sBaseFilename);
+
+-- Restore check constraints.
+ALTER TABLE TestSets ADD CONSTRAINT TestSets_iGangMemberNo_Check CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024);
+
+-- Restore foreign keys in the table.
+ALTER TABLE TestSets ADD FOREIGN KEY (idBuildCategory) REFERENCES BuildCategories(idBuildCategory);
+ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox);
+ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestCase) REFERENCES TestCases(idGenTestCase);
+ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestCaseArgs) REFERENCES TestCaseArgs(idGenTestCaseArgs);
+ALTER TABLE TestSets ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult);
+ALTER TABLE TestSets ADD FOREIGN KEY (idTestSetGangLeader) REFERENCES TestSets(idTestSet);
+
+-- Restore indexes.
+CREATE INDEX TestSetsGangIdx ON TestSets (idTestSetGangLeader);
+CREATE INDEX TestSetsBoxIdx ON TestSets (idTestBox, idTestResult);
+CREATE INDEX TestSetsBuildIdx ON TestSets (idBuild, idTestResult);
+CREATE INDEX TestSetsTestCaseIdx ON TestSets (idTestCase, idTestResult);
+CREATE INDEX TestSetsTestVarIdx ON TestSets (idTestCaseArgs, idTestResult);
+CREATE INDEX TestSetsDoneCreatedBuildCatIdx ON TestSets (tsDone DESC NULLS FIRST, tsCreated ASC, idBuildCategory);
+CREATE INDEX TestSetsGraphBoxIdx ON TestSets (idTestBox, tsCreated DESC, tsDone ASC NULLS LAST, idBuildCategory, idTestCase);
+
+-- Restore foreign key references to the table.
+ALTER TABLE TestResults ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultFiles ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultMsgs ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE TestResultFailures ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+
+ALTER TABLE TestBoxStatuses ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL;
+ALTER TABLE SchedQueues ADD FOREIGN KEY (idTestSetGangLeader) REFERENCES TestSets(idTestSet) MATCH FULL;
+
+-- Drop the old table.
+DROP TABLE OldTestSets;
+
+\prompt "Update python files while everything is locked. Hurry!" dummy
+
+-- Grant access to the new table.
+GRANT ALL PRIVILEGES ON TABLE TestSets TO testmanager;
+
+COMMIT;
+
+\d TestSets;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql
new file mode 100644
index 00000000..8d7d7df0
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql
@@ -0,0 +1,181 @@
+-- $Id: tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Turns idSchedGroup column in TestBoxes
+-- into an N:M relationship with a priority via the new table
+-- TestBoxesInSchedGroups. Adds an internal scheduling table index to
+-- TestBoxStatuses to implement testboxes switching between groups.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Cleanup after failed runs.
+--
+DROP TABLE IF EXISTS OldTestBoxes;
+
+--
+-- Die on error from now on.
+--
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+
+-- Total grid lock.
+LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE SchedGroups IN ACCESS EXCLUSIVE MODE;
+LOCK TABLE SchedGroupMembers IN ACCESS EXCLUSIVE MODE;
+
+\d+ TestBoxes;
+
+--
+-- We'll only be doing simple alterations so, no need to drop constraints
+-- and stuff like we usually do first.
+--
+
+--
+-- Create the new table and populate it.
+--
+
+CREATE TABLE TestBoxesInSchedGroups (
+ --- TestBox ID.
+ -- Non-unique foreign key: TestBoxes(idTestBox).
+ idTestBox INTEGER NOT NULL,
+ --- Scheduling ID.
+ -- Non-unique foreign key: SchedGroups(idSchedGroup).
+ idSchedGroup INTEGER NOT NULL,
+ --- When this row starts taking effect (inclusive).
+ tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL,
+ --- When this row stops being tsEffective (exclusive).
+ tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL,
+ --- The user id of the one who created/modified this entry.
+ -- Non-unique foreign key: Users(uid)
+ uidAuthor INTEGER NOT NULL,
+
+ --- The scheduling priority of the scheduling group for the test box.
+ -- Higher number causes the scheduling group to be serviced more frequently.
+ -- @sa TestGroupMembers.iSchedPriority, SchedGroups.iSchedPriority
+ iSchedPriority INTEGER DEFAULT 16 CHECK (iSchedPriority >= 0 AND iSchedPriority < 32) NOT NULL,
+
+ PRIMARY KEY (idTestBox, idSchedGroup, tsExpire)
+);
+
+GRANT ALL PRIVILEGES ON TABLE TestBoxesInSchedGroups TO testmanager;
+
+CREATE OR REPLACE FUNCTION TestBoxesInSchedGroups_ConvertedOneBox(a_idTestBox INTEGER)
+ RETURNS VOID AS $$
+ DECLARE
+ v_Row RECORD;
+ v_idSchedGroup INTEGER;
+ v_uidAuthor INTEGER;
+ v_tsEffective TIMESTAMP WITH TIME ZONE;
+ v_tsExpire TIMESTAMP WITH TIME ZONE;
+ BEGIN
+ FOR v_Row IN
+ SELECT idTestBox,
+ idSchedGroup,
+ tsEffective,
+ tsExpire,
+ uidAuthor
+ FROM TestBoxes
+ WHERE idTestBox = a_idTestBox
+ ORDER BY tsEffective, tsExpire
+ LOOP
+ IF v_idSchedGroup IS NOT NULL THEN
+ IF (v_idSchedGroup != v_Row.idSchedGroup) OR (v_Row.tsEffective <> v_tsExpire) THEN
+ INSERT INTO TestBoxesInSchedGroups (idTestBox, idSchedGroup, tsEffective, tsExpire, uidAuthor)
+ VALUES (a_idTestBox, v_idSchedGroup, v_tsEffective, v_tsExpire, v_uidAuthor);
+ v_idSchedGroup := NULL;
+ END IF;
+ END IF;
+
+ IF v_idSchedGroup IS NULL THEN
+ v_idSchedGroup := v_Row.idSchedGroup;
+ v_tsEffective := v_Row.tsEffective;
+ END IF;
+ IF v_Row.uidAuthor IS NOT NULL THEN
+ v_uidAuthor := v_Row.uidAuthor;
+ END IF;
+ v_tsExpire := v_Row.tsExpire;
+ END LOOP;
+
+ IF v_idSchedGroup != -1 THEN
+ INSERT INTO TestBoxesInSchedGroups (idTestBox, idSchedGroup, tsEffective, tsExpire, uidAuthor)
+ VALUES (a_idTestBox, v_idSchedGroup, v_tsEffective, v_tsExpire, v_uidAuthor);
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+SELECT TestBoxesInSchedGroups_ConvertedOneBox(TestBoxIDs.idTestBox)
+FROM ( SELECT DISTINCT idTestBox FROM TestBoxes ) AS TestBoxIDs;
+
+DROP FUNCTION TestBoxesInSchedGroups_ConvertedOneBox(INTEGER);
+
+--
+-- Do the other two modifications.
+--
+ALTER TABLE TestBoxStatuses ADD COLUMN iWorkItem INTEGER DEFAULT 0 NOT NULL;
+
+DROP VIEW TestBoxesWithStrings;
+ALTER TABLE TestBoxes DROP COLUMN idSchedGroup;
+CREATE VIEW TestBoxesWithStrings AS
+ SELECT TestBoxes.*,
+ Str1.sValue AS sDescription,
+ Str2.sValue AS sComment,
+ Str3.sValue AS sOs,
+ Str4.sValue AS sOsVersion,
+ Str5.sValue AS sCpuVendor,
+ Str6.sValue AS sCpuArch,
+ Str7.sValue AS sCpuName,
+ Str8.sValue AS sReport
+ FROM TestBoxes
+ LEFT OUTER JOIN TestBoxStrTab Str1 ON idStrDescription = Str1.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str2 ON idStrComment = Str2.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str3 ON idStrOs = Str3.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str4 ON idStrOsVersion = Str4.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str5 ON idStrCpuVendor = Str5.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str6 ON idStrCpuArch = Str6.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str7 ON idStrCpuName = Str7.idStr
+ LEFT OUTER JOIN TestBoxStrTab Str8 ON idStrReport = Str8.idStr;
+
+GRANT ALL PRIVILEGES ON TABLE TestBoxesWithStrings TO testmanager;
+
+\prompt "Update python files while everything is locked. Hurry!" dummy
+
+COMMIT;
+
+\d TestBoxesInSchedGroups;
+\d TestBoxStatuses;
+\d TestBoxes;
+ANALYZE VERBOSE TestBoxesInSchedGroups;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r23-users-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r23-users-2.pgsql
new file mode 100644
index 00000000..7a919da3
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r23-users-2.pgsql
@@ -0,0 +1,60 @@
+-- $Id: tmdb-r23-users-2.pgsql $
+--- @file
+-- VBox Test Manager Database - Adds fReadOnly column to Users.
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Cleanup after failed runs.
+--
+DROP TABLE IF EXISTS OldTestBoxes;
+
+--
+-- Die on error from now on.
+--
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+
+
+-- This change can be implemented using ALTER TABLE. Yeah!
+\d+ Users;
+
+ALTER TABLE Users
+ ADD COLUMN fReadOnly BOOLEAN NOT NULL DEFAULT FALSE;
+
+COMMIT;
+
+\d Users;
+ANALYZE VERBOSE Users;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r24-vcsbugreferences-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r24-vcsbugreferences-1.pgsql
new file mode 100644
index 00000000..0bb92ed4
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r24-vcsbugreferences-1.pgsql
@@ -0,0 +1,59 @@
+-- $Id: tmdb-r24-vcsbugreferences-1.pgsql $
+--- @file
+-- VBox Test Manager Database - Creates a new VcsBugReferences table.
+--
+
+--
+-- Copyright (C) 2020-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+-- Die on error from now on.
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 0
+
+--
+-- Create the new VcsBugReferences table.
+--
+CREATE TABLE VcsBugReferences (
+ --- The version control tree name.
+ sRepository TEXT NOT NULL,
+ --- The version control tree revision number.
+ iRevision INTEGER NOT NULL,
+ --- The bug tracker identifier - see g_kdBugTrackers in config.py.
+ sBugTracker CHAR(4) NOT NULL,
+ --- The bug number in the bug tracker.
+ lBugNo BIGINT NOT NULL,
+
+ UNIQUE (sRepository, iRevision, sBugTracker, lBugNo)
+);
+CREATE INDEX VcsBugReferencesLookupIdx ON VcsBugReferences (sBugTracker, lBugNo);
+COMMIT;
+\d+ VcsBugReferences;
+
diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r25-vcsrevisions-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r25-vcsrevisions-2.pgsql
new file mode 100644
index 00000000..f0c4e2bd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r25-vcsrevisions-2.pgsql
@@ -0,0 +1,45 @@
+-- $Id: tmdb-r25-vcsrevisions-2.pgsql $
+--- @file
+-- VBox Test Manager Database - Creates a new index on VcsRevisions
+--
+
+--
+-- Copyright (C) 2013-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+--
+-- Die on error from now on.
+--
+\set ON_ERROR_STOP 1
+\set AUTOCOMMIT 1
+
+
+CREATE INDEX VcsRevisionsByDate ON VcsRevisions (tsCreated DESC);
+
diff --git a/src/VBox/ValidationKit/testmanager/debug/Makefile.kmk b/src/VBox/ValidationKit/testmanager/debug/Makefile.kmk
new file mode 100644
index 00000000..74d882cc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/debug/Makefile.kmk
@@ -0,0 +1,46 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+SUB_DEPTH = ../../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+
+VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py)
+
+$(evalcall def_vbox_validationkit_process_python_sources)
+$(evalcall def_vbox_validationkit_process_js_sources)
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/testmanager/debug/__init__.py b/src/VBox/ValidationKit/testmanager/debug/__init__.py
new file mode 100644
index 00000000..1a97eacf
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/debug/__init__.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# $Id: __init__.py $
+
+"""
+Test Manager - Debug Utilities.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
diff --git a/src/VBox/ValidationKit/testmanager/debug/add_testbox.pgsql b/src/VBox/ValidationKit/testmanager/debug/add_testbox.pgsql
new file mode 100644
index 00000000..faf303c2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/debug/add_testbox.pgsql
@@ -0,0 +1,76 @@
+-- $Id: add_testbox.pgsql $
+--- @file
+-- Test data.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+\connect testmanager;
+
+INSERT INTO users (sUsername, sNickname, sFullName, sLoginName)
+ VALUES ('testmanager', 'testmanager', 'testmanager', 'testmanager');
+
+INSERT INTO testboxes (uidAuthor,
+ ip,
+ uuidSystem,
+ sName,
+ fEnabled,
+ sOs,
+ sOsVersion,
+ sCpuVendor,
+ sCpuArch,
+ cCpus,
+ fCpuHwVirt,
+ fCpuNestedPaging,
+ fCpu64BitGuest,
+ fChipsetIoMmu,
+ cMbMemory,
+ cMbScratch)
+
+ VALUES (1,
+ '127.0.0.1',
+ '9394c36a-cb2f-3c7f-b987-9f54a4519bbc',
+ 'localhost',
+ TRUE,
+ 'LINUX',
+ '1.0',
+ 'Intel',
+ 'AMD64',
+ 2,
+ TRUE,
+ TRUE,
+ TRUE,
+ TRUE,
+ 1024,
+ 1024);
+
diff --git a/src/VBox/ValidationKit/testmanager/debug/cgiprofiling.py b/src/VBox/ValidationKit/testmanager/debug/cgiprofiling.py
new file mode 100755
index 00000000..be049e41
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/debug/cgiprofiling.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# $Id: cgiprofiling.py $
+
+"""
+Debug - CGI Profiling.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+def profileIt(fnMain, sAppendToElement = 'main', sSort = 'time'):
+ """
+ Profiles a main() type function call (no parameters, returns int) and
+ outputs a hacky HTML section.
+ """
+
+ #
+ # Execute it.
+ #
+ import cProfile;
+ oProfiler = cProfile.Profile();
+ rc = oProfiler.runcall(fnMain);
+
+ #
+ # Output HTML to stdout (CGI assumption).
+ #
+ print('<div id="debug2"><br>\n' # Lazy BR-layouting!!
+ ' <h2>Profiler Output</h2>\n'
+ ' <pre>');
+ try:
+ oProfiler.print_stats(sort = sSort);
+ except Exception as oXcpt:
+ print('<p><pre>%s</pre></p>\n' % (oXcpt,));
+ else:
+ print('</pre>\n');
+ oProfiler = None;
+ print('</div>\n');
+
+ #
+ # Trick to move the section in under the SQL trace.
+ #
+ print('<script lang="script/javascript">\n'
+ 'var oMain = document.getElementById(\'%s\');\n'
+ 'if (oMain) {\n'
+ ' oMain.appendChild(document.getElementById(\'debug2\'));\n'
+ '}\n'
+ '</script>\n'
+ % (sAppendToElement, ) );
+
+ return rc;
+
diff --git a/src/VBox/ValidationKit/testmanager/debug/functions.pgsql b/src/VBox/ValidationKit/testmanager/debug/functions.pgsql
new file mode 100644
index 00000000..00854e91
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/debug/functions.pgsql
@@ -0,0 +1,82 @@
+-- $Id: functions.pgsql $
+--- @file
+-- ?????????????????????????
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+\connect testmanager;
+
+DROP FUNCTION authenticate_testbox(inet, uuid);
+DROP FUNCTION testbox_status_set(integer, TestBoxState_T);
+
+-- Authenticate Test Box record by IP and UUID and set its state to IDLE
+-- Args: IP, UUID
+CREATE OR REPLACE FUNCTION authenticate_testbox(inet, uuid) RETURNS testboxes AS $$
+ DECLARE
+ _ip ALIAS FOR $1;
+ _uuidSystem ALIAS FOR $2;
+ _box TestBoxes;
+ BEGIN
+ -- Find Test Box record
+ SELECT *
+ FROM testboxes
+ WHERE ip=_ip AND uuidSystem=_uuidSystem INTO _box;
+ IF FOUND THEN
+ -- Update Test Box status if exists
+ UPDATE TestBoxStatuses SET enmState='idle' WHERE idTestBox=_box.idTestBox;
+ IF NOT FOUND THEN
+ -- Otherwise, add new record to TestBoxStatuses table
+ INSERT
+ INTO TestBoxStatuses(idTestBox, idGenTestBox, enmState)
+ VALUES (_box.idTestBox, _box.idGenTestBox, 'idle');
+ END IF;
+ END IF;
+ return _box;
+ END;
+$$ LANGUAGE plpgsql;
+
+-- Set Test Box status and make sure if it has been set
+-- Args: Test Box ID, new status
+CREATE OR REPLACE FUNCTION testbox_status_set(integer, TestBoxState_T) RETURNS VOID AS $$
+ DECLARE
+ _box ALIAS FOR $1;
+ _status ALIAS FOR $2;
+ BEGIN
+ -- Update Test Box status if exists
+ UPDATE TestBoxStatuses SET enmState=_status WHERE idTestBox=_box;
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'Test Box (#%) was not found in database', _box;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup b/src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/common.css b/src/VBox/ValidationKit/testmanager/htdocs/css/common.css
new file mode 100644
index 00000000..9ccf7a54
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/css/common.css
@@ -0,0 +1,1183 @@
+/* $Id: common.css $ */
+/** @file
+ * Test Manager - Common CSS.
+ */
+
+/*
+ * Copyright (C) 2012-2023 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+ * in the VirtualBox distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+ */
+
+@charset "UTF-8";
+
+/*
+ * Basic HTML elements.
+ */
+* {
+ margin: 0;
+ padding: 0;
+}
+
+html, body {
+ height: 100%;
+}
+
+body {
+ background: #f9f9f9 repeat-y center;
+ font-family: Georgia, "Times New Roman", Times, serif;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 0.8em;
+ color: #2f2f2f;
+}
+
+p, ul, ol {
+ margin-top: 0;
+}
+
+div {
+ margin: 0;
+ padding: 0;
+}
+
+h1, h2, h3 {
+ margin: 0px 0 10px 0;
+ padding: 0;
+ font-weight: normal;
+ color: #2f2f2f;
+ line-height: 180%;
+}
+h1 {
+ font-size: 2.4em;
+}
+h2 {
+ font-size: 2.0em;
+}
+h3 {
+ font-size: 1.5em;
+}
+
+dl {
+ margin-bottom: 10px;
+}
+
+
+/*
+ * Misc class stuff.
+ */
+.clear {
+ clear: both;
+}
+
+.left {
+ float: left;
+}
+
+.right {
+ float: right;
+}
+
+
+
+/*
+ * The general layout.
+ *
+ * Note! Not quite sure if something like this will work well everywhere...
+ * Will get back to that when the logic and content is all there, not
+ * worth wasting more time on CSS now.
+ */
+
+html, body {
+ height: 100%;
+}
+
+#wrap {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+#head-wrap {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 74px; /**< header + top-menu. */
+ width: 100%;
+ background: #f9f9f9;
+}
+
+#logo {
+ width: 42px;
+ height: 46px;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: auto;
+ /* Center the image in both directions. */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ justify-content: flex-end;
+}
+
+#logo img {
+ height: 36px;
+ width: 36px;
+}
+
+#header {
+ position: fixed;
+ width: 100%; /** @todo this is too wide, darn! */
+ height: 46px;
+ left: 42px;
+ top: 0;
+ right: 0;
+ bottom: auto;
+ margin-top: 0px;
+ margin-left: 0px;
+ text-align: left;
+ /* Center the h1 child vertically: */
+ display: flex;
+ align-items: center;
+}
+
+#login {
+ position: absolute;
+ top: 0;
+ left: auto;
+ right: 2px;
+ bottom: auto;
+ height: auto;
+}
+
+#top-menu {
+ position: fixed;
+ padding: 0px;
+ width: 99%;
+ height: auto;
+ max-height: 22px;
+ top: 46px;
+ left: 0px;
+ right: 0px;
+ bottom: auto;
+}
+
+body.tm-wide-side-menu #side-menu-wrap {
+ width: 300px;
+}
+#side-menu-wrap {
+ position: fixed;
+ top: 0px;
+ left: 0;
+ right: auto;
+ bottom: auto;
+
+ width: 164px;
+ height: 100vh;
+ min-height: 100vh;
+ max-height: 100vh;
+
+ display: flex;
+}
+
+#side-menu {
+ margin-top: 46px;
+ margin-top: 70px;
+ padding-top: 6px
+ height: auto;
+ max-height: 100%;
+ width: 95%;
+ width: calc(100% - 8px); /* CSS3 */
+
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+#side-menu-body {
+ display: block;
+ max-height: 100%;
+ overflow: auto;
+}
+
+body.tm-wide-side-menu #main {
+ margin-left: 300px;
+}
+#main {
+ height: 100%;
+ margin-top: 74px; /**< header + top-menu + padding. */
+ margin-left: 164px;
+ padding-left: 2px;
+ padding-right: 2px;
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+
+/*
+ * Header and logo specifics.
+ */
+#header h1 {
+ margin-left: 8px;
+ margin-top: 0px;
+ margin-right: 0px;
+ margin-bottom: 0px;
+ font-weight: bold;
+ font-size: 2.2em;
+ font-family: Times New, Times, serif;
+}
+
+#login p {
+ line-height: 100%;
+}
+
+
+/*
+ * Navigation menus (common).
+ */
+#top-menu, #side-menu {
+ font-weight: bold;
+ font-size: 1em;
+ font-family: Arial, Helvetica, sans-serif;
+ background-color: #c0d0e0;
+ padding: 2px 2px 2px 2px;
+}
+
+#top-menu.tm-top-menu-wo-side {
+ border-radius: 12px;
+}
+#top-menu {
+ border-radius: 12px 12px 12px 0px;
+}
+
+#side-menu {
+ border-radius: 0px 0px 12px 12px;
+}
+
+#head-wrap {
+ line-height: 180%;
+}
+
+#top-menu ul li a, #side-menu ul li a {
+ text-decoration: none;
+ color: #000000;
+ font-weight: bold;
+ font-size: 1em;
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+#top-menu a:hover, #top-menu .current_page_item a, #side-menu a:hover, #side-menu .current_page_item a {
+ text-decoration: none;
+ color: #b23c1c;
+}
+
+
+/*
+ * Navigation in on the left side.
+ */
+
+
+/* Side menu: */
+#side-menu {
+ /* margin-top and padding-top are set up in layout !*/
+ margin-right: 3px;
+ margin-left: 3px;
+ margin-bottom: 3px;
+}
+
+#side-menu p {
+ margin-right: 3px;
+ margin-left: 3px;
+}
+
+#side-menu ul {
+ list-style: none;
+ margin-left: 3px;
+ margin-right: 3px;
+}
+
+#side-menu li {
+ padding-top: 0.3em;
+ padding-bottom: 0.3em;
+ line-height: 1.0em;
+ text-align: left;
+}
+
+#side-menu .subheader_item {
+ font-style: italic;
+ font-size: 1.1em;
+ text-decoration: underline;
+}
+
+.subheader_item:not(:first-child) {
+ margin-top: 0.5em;
+}
+
+/* The following is for the element of / not element of checkbox, supplying text and hiding the actual box. */
+input.tm-side-filter-union-input {
+ display: none;
+}
+input.tm-side-filter-union-input + label {
+ vertical-align: middle;
+}
+input.tm-side-filter-union-input[type=checkbox]:checked + label::after {
+ content: '∉'; /* U+2209: not an element of. */
+}
+input.tm-side-filter-union-input[type=checkbox] + label::after {
+ content: '∈'; /* U+2208: element of. */
+}
+
+/* Webkit: Pretty scroll bars on the menu body as well as inside filter criteria. */
+#side-menu ::-webkit-scrollbar {
+ width: 8px;
+}
+#side-menu ::-webkit-scrollbar-track {
+ -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3);
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+}
+#side-menu ::-webkit-scrollbar-thumb {
+ -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5);
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ background: rgba(112, 128, 144, 0.9);
+}
+#side-menu ::-webkit-scrollbar-thumb:window-inactive {
+ background: rgba(112, 128, 144, 0.7);
+}
+
+/* Filters: */
+.tm-side-filter-title-buttons {
+ float: right;
+}
+body.tm-wide-side-menu .tm-side-filter-title-buttons input {
+ display: none;
+}
+.tm-side-filter-title-buttons input {
+ display: inline;
+}
+.tm-side-filter-title-buttons input {
+ font-size: 0.6em;
+}
+.tm-side-filter-dt-buttons input {
+ font-size: 0.6em;
+}
+body.tm-wide-side-menu .tm-side-filter-dt-buttons input[type=submit] {
+ display: inline;
+}
+.tm-side-filter-dt-buttons input[type=submit] {
+ display: none;
+}
+.tm-side-filter-dt-buttons {
+ float: right;
+}
+
+#side-filters p:first-child {
+ margin-top: 0.5em;
+ font-style: italic;
+ font-size: 1.1em;
+ text-decoration: underline;
+}
+
+#side-filters dd.sf-collapsible {
+ display: block;
+}
+
+#side-filters dd.sf-expandable {
+ display: none;
+}
+
+#side-filters a {
+ text-decoration: none;
+ color: #000000;
+}
+
+#side-filters dt {
+ margin-top: 0.4em;
+}
+
+#side-filters dd {
+ font-size: 0.82em;
+ font-family: "Arial Narrow", Arial, sans-serif;
+ font-weight: normal;
+ clear: both; /* cancel .tm-side-filter-dt-buttons */
+}
+
+#side-filters li, #side-filters input[type=checkbox], #side-filters p {
+ line-height: 0.9em;
+ vertical-align: text-bottom;
+}
+
+#side-filters input[type=checkbox] {
+ margin-right: 0.20em;
+ width: 1.0em;
+ height: 1.0em;
+}
+@supports(-moz-appearance:meterbar) {
+ #side-filters input[type=checkbox] {
+ /* not currently used */
+ }
+}
+@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { /* IE 10+ specific tweaks */
+ #side-filters input[type=checkbox] {
+ width: 1.1em;
+ height: 1.1em;
+ }
+}
+
+#side-filters dd > ul {
+ max-height: 22em;
+ overflow: auto;
+}
+
+#side-filters ul ul {
+ margin-left: 1.4em;
+}
+
+#side-filters li {
+ padding-top: 1px;
+ padding-bottom: 1px;
+ overflow-wrap: break-word;
+}
+
+ul.sf-checkbox-collapsible {
+ display: block;
+}
+
+ul.sf-checkbox-expandable {
+ display: none;
+}
+
+.side-filter-irrelevant {
+ font-style: italic;
+ font-weight: normal;
+}
+.side-filter-count {
+ font-size: smaller;
+ vertical-align: text-top;
+}
+
+/* Footer: */
+#side-footer {
+ width: 100%;
+ margin-left: 2px;
+ margin-right: 2px;
+ margin-top: 1em;
+ padding-top: 1em;
+ padding-bottom: 0.8em;
+ border-top: thin white ridge;
+}
+
+#side-footer p {
+ margin-left: 3px;
+ margin-right: 3px;
+ margin-bottom: 0.5em;
+ font-family: Times New, Times, serif;
+ font-size: 0.86em;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1.2em;
+ text-align: center;
+}
+
+
+/*
+ * Navigation in the header.
+ */
+#top-menu {
+ margin-right: 3px; /* same as #side-menu! */
+ margin-left: 3px;
+}
+
+#top-menu ul li a {
+ padding: .1em 1em;
+}
+
+#top-menu ul li {
+ display: inline;
+}
+
+#top-menu ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ list-style-type: none;
+ text-align: center;
+}
+
+#top-menu a {
+ border: none;
+}
+
+#top-menu .current_page_item a {
+}
+
+/*
+ * Time navigation forms on a line with some padding between them.
+ */
+.tmtimenav form {
+ display: inline-block;
+}
+
+.tmtimenav form + form {
+ padding-left: 0.6em;
+}
+
+/*
+ * Items per page and next.
+ */
+.tmnextanditemsperpage form {
+ display: inline-block;
+ padding-left: 1em;
+}
+
+/*
+ * Error message (typically a paragraph in the body).
+ */
+.tmerrormsg {
+ color: #ff0000;
+ white-space: pre;
+ font-family: Monospace, "Lucida Console", "Courier New", "Courier";
+ display: block;
+ border: 1px solid;
+ margin: 1em;
+ padding: 0.6em;
+}
+
+
+/*
+ * Generic odd/even row and sub-row attribs.
+ */
+.tmeven {
+ background-color: #ececec;
+}
+
+.tmodd {
+ background-color: #fcfcfc;
+}
+
+/** @todo adjust the sub row colors (see change logs for examples). */
+.tmeveneven {
+ background-color: #d8e0f8;
+}
+
+.tmevenodd {
+ background-color: #e8f0ff;
+}
+
+.tmoddeven {
+ background-color: #d8e0f8;
+}
+
+.tmoddodd {
+ background-color: #e8f0ff;
+}
+
+/*
+ * Multi color row/item coloring, 0..7.
+ */
+.tmshade0 { background-color: #ececec; }
+.tmshade1 { background-color: #fbfbfb; }
+.tmshade2 { background-color: #e4e4e4; }
+.tmshade3 { background-color: #f4f4f4; }
+.tmshade4 { background-color: #e0e0e0; }
+.tmshade5 { background-color: #f0f0f0; }
+.tmshade6 { background-color: #dcdcdc; }
+.tmshade7 { background-color: #fdfdfd; }
+
+
+/*
+ * Generic thead class (first-child doesn't work for multiple header rows).
+ */
+.tmheader {
+ background-color: #d0d0d0;
+ color: black;
+}
+
+/*
+ * Generic class for div elements wrapping pre inside a table. This prevents
+ * the <pre> from taking up way more screen space that available.
+ */
+.tdpre {
+ display: table;
+ table-layout: fixed;
+ width: 100%;
+}
+.tdpre pre {
+ overflow: auto;
+}
+
+
+/*
+ * A typical table.
+ */
+/* table.tmtable th {
+ background-color: #d0d0d0;
+ color: black;
+} */
+
+table.tmtable caption {
+ text-align: left;
+}
+
+table.tmtable {
+ width: 100%;
+ border-spacing: 0px;
+}
+
+table.tmtable th {
+ font-size: 1.3em;
+ text-align: center;
+}
+
+table.tmtable, table.tmtable tr, table.tmtable td, table.tmtable th {
+ vertical-align: top;
+}
+
+table.tmtable {
+ border-left: 1px solid black;
+ border-top: 1px solid black;
+ border-right: none;
+ border-bottom: none;
+}
+
+table.tmtable td, table.tmtable th {
+ border-left: none;
+ border-top: none;
+ border-right: 1px solid black;
+ border-bottom: 1px solid black;
+}
+
+table.tmtable td {
+ padding-left: 3px;
+ padding-right: 3px;
+ padding-top: 3px;
+ padding-bottom: 3px;
+}
+
+table.tmtable th {
+ padding-left: 3px;
+ padding-right: 3px;
+ padding-top: 6px;
+ padding-bottom: 6px;
+}
+
+.tmtable td {
+}
+
+tr.tmseparator td {
+ border-bottom: 2px solid black;
+ font-size: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+
+
+/*
+ * Table placed inside of a big table used to display *all* stuff of a category.
+ */
+
+table.tminnertbl tr:nth-child(odd) {
+ background-color: #e8e8e8;
+}
+table.tminnertbl tr:nth-child(even) {
+ background-color: #f8f8f8;
+}
+table.tminnertbl tr:first-child {
+ background-color: #d0d0d0;
+ color: black;
+}
+
+table.tminnertbl {
+ border-style: dashed;
+ border-spacing: 1px;
+ border-width: 1px;
+ border-color: gray;
+ border-collapse: separate;
+}
+
+table.tminnertbl th, table.tminnertbl td {
+ font-size: 1em;
+ text-align: center;
+ border-style: none;
+ padding: 1px;
+ border-width: 1px;
+ border-color: #FFFFF0;
+}
+
+/*
+ * Table placed inside a form.
+ */
+table.tmformtbl {
+ border-style: none;
+ border-spacing: 1px;
+ border-width: 1px;
+ border-collapse: separate;
+}
+
+table.tmformtbl th, table.tmformtbl td {
+ font-size: 1em;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+ padding-bottom: 1px;
+ padding-top: 1px;
+ border-width: 1px;
+}
+
+table.tmformtbl th, table.tmformtbl thead {
+ background-color: #d0d0d0;
+ font-size: 1em;
+ font-weight: bold;
+}
+
+table.tmformtbl tr.tmodd {
+ background: #e2e2e2;
+}
+
+table.tmformtblschedgroupmembers tr td:nth-child(3),
+table.tmformtblschedgroupmembers tr td:nth-child(4) {
+ text-align: center;
+}
+
+
+/*
+ * Change log table (used with tmtable).
+ */
+table.tmchangelog > tbody {
+ font-size: 1em;
+}
+
+table.tmchangelog tr.tmodd td:nth-child(1),
+table.tmchangelog tr.tmeven td:nth-child(1),
+table.tmchangelog tr.tmodd td:nth-child(2),
+table.tmchangelog tr.tmeven td:nth-child(2) {
+ min-width: 5em;
+ max-width: 10em; /* futile */
+}
+
+table.tmchangelog tr.tmeven {
+ background-color: #e8f0ff;
+}
+
+table.tmchangelog tr.tmodd {
+ background-color: #d8e0f8;
+}
+
+table.tmchangelog tr.tmoddeven, table.tmchangelog tr.tmeveneven {
+ background-color: #fcfcfc;
+}
+
+table.tmchangelog tr.tmoddodd, table.tmchangelog tr.tmevenodd {
+ background-color: #ececec;
+}
+
+table.tmchangelog tr.tmoddeven, table.tmchangelog tr.tmeveneven, table.tmchangelog tr.tmoddodd, table.tmchangelog tr.tmevenodd {
+ font-size: 0.86em;
+}
+
+.tmsyschlogattr {
+ font-size: 0.80em;
+}
+
+.tmsyschlogspacer {
+ width: 0.8em;
+}
+
+td.tmsyschlogspacer:not(:last-child) {
+ width: 1.8em;
+ border-bottom: 0px solid green !important;
+}
+
+.tmsyschlogevent {
+ border-bottom: 0px solid green !important;
+}
+
+.tmsyschlogspacerrowabove {
+ height: 0.22em;
+}
+
+.tmsyschlogspacerrowbelow {
+ height: 0.80em;
+}
+
+
+/*
+ * Elements to be shows on *Show All* pages.
+ */
+
+ul.tmshowall {
+ margin-left: 15px;
+ margin-right: 15px;
+}
+
+li.tmshowall {
+ margin-left: 5px;
+ margin-right: 5px;
+}
+
+
+/*
+ * List navigation table
+ */
+table.tmlistnavtab {
+ width: 100%;
+}
+
+table.tmlistnavtab tr td:nth-child(1) {
+ text-align: left;
+}
+
+table.tmlistnavtab tr td:nth-child(2) {
+ text-align: right;
+}
+
+
+/*
+ * A typical form.
+ *
+ * Note! This _has_ to be redone. It sucks for the wide fields and such.
+ */
+.tmform ul {
+ list-style: none;
+ list-style-type: none;
+}
+
+.tmform li {
+ line-height: 160%;
+}
+
+
+.tmform-field {
+ display: block;
+ clear: both;
+}
+
+.tmform-field label {
+ float: left;
+ text-align: right;
+ width: 20%;
+ min-width: 10em;
+ max-width: 16em;
+ padding-right: 0.9em;
+}
+
+.tmform-error-desc {
+ display: block;
+ color: #ff0000;
+ font-style: italic;
+}
+
+.tmform-button {
+ float: left;
+ padding-top: 0.8em;
+}
+
+.tmform-field input {
+}
+
+.tmform-field-tiny-int input {
+ width: 2em;
+}
+
+.tmform-field-int input {
+ width: 6em;
+}
+
+.tmform-field-long input {
+ width: 9em;
+}
+
+.tmform-field-submit input {
+}
+
+.tmform-field-string input {
+ width: 24em;
+}
+
+.tmform-field-subname input {
+ width: 10em;
+}
+
+.tmform-field-timestamp input {
+ width: 20em;
+}
+
+.tmform-field-uuid input {
+ width: 24em;
+}
+
+.tmform-field-wide input {
+ width: 78%;
+ overflow: hidden;
+}
+
+.tmform-field-wide100 input {
+ width: 100%;
+ overflow: hidden;
+}
+
+.tmform-field-list {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+.tmform-checkboxes-container {
+ padding: 3px;
+ overflow: auto;
+ border: 1px dotted #cccccc;
+}
+
+.tmform-checkbox-holder {
+ float: left;
+ min-width: 20em;
+}
+
+#tmform-checkbox-list-os-arches .tmform-checkbox-holder {
+ min-width: 11em;
+}
+
+#tmform-checkbox-list-build-types .tmform-checkbox-holder {
+ min-width: 6em;
+}
+
+.tmform-input-readonly {
+ background: #ADD8EF;
+ color: #ffffff;
+}
+
+/* (Test case argument variation.) */
+
+table.tmform-innertbl {
+ border-style: none;
+ border-spacing: 1px;
+ border-width: 1px;
+ border-collapse: separate;
+ width: 78%;
+}
+
+table.tmform-innertbl caption {
+ text-align: left;
+}
+
+table.tmform-innertbl th, table.tmform-innertbl td {
+ font-size: 1em;
+ text-align: center;
+ border-style: none;
+ /* padding-top: 1px;*/
+ /*padding-bottom: 1px;*/
+ padding-left: 2px;
+ padding-right: 2px;
+ border-width: 1px;
+ border-color: #FFFFF0;
+ background-color: #f9f9f9;
+}
+
+.tmform-inntertbl-td-wide input {
+ width: 100%;
+ overflow: hidden;
+}
+
+.tmform-inntertbl-td-wide {
+ width: 100%;
+}
+
+
+/*
+ * The test case argument variation table.
+ */
+table.tmform-testcasevars {
+ border-style: none;
+ border-spacing: 0px;
+ border-width: 0px;
+ border-collapse: collapse;
+ width: 78%;
+}
+
+table.tmform-testcasevars tbody {
+ border-style: solid;
+ border-spacing: 1px;
+ border-width: 1px;
+ margin: 2px;
+}
+
+table.tmform-testcasevars td {
+ padding-right: 3px;
+ padding-left: 3px;
+}
+
+table.tmform-testcasevars td:first-child, table.tmform-testcasevars td:nth-child(3) {
+ width: 8em;
+ text-align: right;
+}
+table.tmform-testcasevars td:nth-child(5) {
+ width: 4em;
+ text-align: left;
+}
+
+
+.tmform-testcasevars caption {
+ text-align: left;
+}
+
+tr.tmform-testcasevars-first-row td {
+ padding-top: 0px;
+ padding-bottom: 0px;
+ background-color: #e3e3ec;
+}
+
+.tmform-testcasevars-inner-row td {
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
+
+tr.tmform-testcasevars-final-row td {
+ padding-top: 0px;
+ padding-bottom: 1px;
+}
+
+td.tmform-testcasevars-stupid-border-column {
+ /* Stupid hack. */
+ min-width: 2px;
+ width: 0.1%;
+}
+
+
+
+/*
+ * Log viewer.
+ */
+.tmlog a[href] {
+ background-color: #e0e0e0;
+ padding-left: 0.8em;
+ padding-right: 0.8em;
+}
+
+.tmlog pre {
+ background-color: #000000;
+ color: #00ff00;
+ font-family: "Monospace", "Lucida Console", "Courier New", "Courier";
+}
+
+
+/*
+ * Debug SQL traceback.
+ */
+#debug, #debug h1, #debug h2, #debug h3,
+#debug2, #debug2 h1, #debug2 h2, #debug2 h3 {
+ color: #00009f;
+}
+
+table.tmsqltable {
+ border-collapse: collapse;
+}
+
+table.tmsqltable, table.tmsqltable tr, table.tmsqltable td, table.tmsqltable th {
+ border: 1px solid;
+ vertical-align: middle;
+ padding: 0.1ex 0.5ex;
+}
+
+table.tmsqltable pre {
+ text-align: left;
+}
+
+table.tmsqltable tr td {
+ text-align: left;
+}
+
+table.tmsqltable tr td:nth-child(1),
+table.tmsqltable tr td:nth-child(2),
+table.tmsqltable tr td:nth-child(3),
+table.tmsqltable tr td:nth-child(4) {
+ text-align: right;
+}
+
+
+
+/*
+ * Various more or less common span classes.
+ */
+.tmspan-offline {
+ color: #f08020;
+ font-size: 0.75em;
+}
+
+.tmspan-online {
+ font-size: 0.75em;
+}
+
+.tmspan-name, .tmspan-osarch {
+ font-weight: bold;
+}
+
+.tmspan-osver1 {
+ font-style: italic;
+}
+
+.tmspan-osver2 {
+ font-style: normal;
+}
+
+
+/*
+ * Subversion tooltip.
+ */
+.tmvcstooltip {
+ padding: 0px;
+ min-width: 50em;
+ overflow: hidden;
+ border: 0px none;
+}
+
+.tmvcstooltip iframe {
+ padding: 0px;
+ margin: 0px;
+ border: 0px none;
+ width: 100%;
+ //overflow: auto;
+ overflow: hidden;
+}
+
+.tmvcstooltipnew {
+ padding: 0px;
+ min-width: 50em;
+ overflow: hidden;
+ border: 0px none;
+ background-color: #f9f9f9;
+}
+
+
+/*
+ * Workaround for flickering tooltips in the column bar graphs (see
+ * https://github.com/google/google-visualization-issues/issues/2162).
+ */
+.google-visualization-tooltip {
+ pointer-events: none;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/details.css b/src/VBox/ValidationKit/testmanager/htdocs/css/details.css
new file mode 100644
index 00000000..1ae05671
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/css/details.css
@@ -0,0 +1,216 @@
+/* $Id: details.css $ */
+/** @file
+ * Test Manager - Test Details CSS.
+ */
+
+/*
+ * Copyright (C) 2012-2023 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+ * in the VirtualBox distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+ */
+
+
+
+/*
+ * The test details page has no side menu, so adjust the top-menu and main
+ * sections so they start at the left border.
+ */
+
+#top-menu, #main {
+ left: 0;
+}
+#main {
+ margin-left: 0px;
+}
+
+.tmtbl-events {
+
+}
+
+.tmstatusrow-failure, .tmstatusrow-timed-out, .tmstatusrow-rebooted {
+ color: #e80000;
+}
+
+.tmstatusrow-skipped, .tmstatusrow-aborted, .tmstatusrow-bad-testbox {
+ color: #0000f0;
+}
+
+
+/*
+ * Test results.
+ */
+
+/*
+ * Details table on the individual test result page.
+ */
+table.tmtbl-testresult-details {
+ border-style: dashed;
+ border-spacing: 1px;
+ border-width: 1px;
+ border-color: gray;
+ border-collapse: separate;
+}
+
+table.tmtbl-testresult-details caption {
+ text-align: left;
+ font-weight: bold;
+ font-size: 1.2em;
+}
+
+table.tmtbl-testresult-details td, table.tmtbl-testresult-details th {
+ font-size: 1em;
+ border-style: none;
+ padding-bottom: 3px;
+ padding-top: 3px;
+ padding-left: 2px;
+ padding-right: 2px;
+ border-width: 1px;
+}
+
+table.tmtbl-testresult-details th {
+ text-align: left;
+}
+
+.tmtbl-result-details-caption {
+ font-size: 1.2em;
+ font-weight: bold;
+ text-align: center;
+ background-color: #c0d0e0;
+}
+
+.tmtbl-result-details-subcaption {
+ text-align: center;
+}
+
+
+/*
+ * Event log on the individual test result page.
+ */
+.tmtbl-events td {
+ padding-bottom: 1px;
+ padding-top: 1px;
+ padding-left: 1px;
+ padding-right: 1px;
+ vertical-align: top;
+}
+
+.tmtbl-events th {
+ font-size: 1.3em;
+ text-align: center;
+}
+
+table.tmtbl-events, table.tmtbl-events tr, table.tmtbl-events td, table.tmtbl-events th {
+ border-collapse: collapse;
+}
+
+tr.tmtbl-events-leaf {
+}
+
+tr.tmtbl-events-first {
+ border-top: 1px dotted;
+}
+
+tr.tmtbl-events-value {
+}
+
+tr.tmtbl-events-final {
+ border-bottom: 1px dotted;
+}
+
+
+tr.tmtbl-events-lvl0 td {
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+tr.tmtbl-events-lvl1 td {
+ padding-top: 6px;
+ padding-bottom: 6px;
+}
+
+tr.tmtbl-events-lvl2 td {
+ padding-top: 4px;
+ padding-bottom: 4px;
+}
+
+tr.tmtbl-events-lvl3 td {
+ padding-top: 2px;
+ padding-bottom: 2px;
+}
+
+tr.tmtbl-events-lvl4 td {
+ padding-top: 1px;
+ padding-bottom: 1px;
+}
+
+tr.tmtbl-events-lvl5 td,
+tr.tmtbl-events-lvl6 td,
+tr.tmtbl-events-lvl7 td,
+tr.tmtbl-events-lvl8 td,
+tr.tmtbl-events-lvl9 td,
+tr.tmtbl-events-lvl10 td {
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
+
+td.tmtbl-events-number {
+ text-align: right;
+}
+
+td.tmtbl-events-number, td.tmtbl-events-unit {
+}
+
+tr.tmtbl-events-value td:nth-child(3),
+tr.tmtbl-events-file td:nth-child(3),
+tr.tmtbl-events-message td:nth-child(3) {
+ padding-left: 2em;
+}
+
+tr.tmtbl-events-value td:nth-child(3),
+tr.tmtbl-events-message td:nth-child(3) {
+ font-style: italic;
+}
+
+
+/*
+ * Status coloring. (move to common.css?)
+ */
+.tmspan-status-success {
+ color: green;
+}
+.tmspan-status-skipped {
+ color: blue;
+}
+.tmspan-status-failure {
+ color: red;
+}
+.tmspan-status-success, .tmspan-status-skipped, .tmspan-status-failure {
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css b/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css
new file mode 100644
index 00000000..2354bfc1
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css
@@ -0,0 +1,237 @@
+/* $Id: graphwiz.css $ */
+/** @file
+ * Test Manager - Graph Wizard CSS.
+ */
+
+/*
+ * Copyright (C) 2012-2023 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+ * in the VirtualBox distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+ */
+
+
+
+/*
+ * The graph wizard page currently has no side menu, so adjust the top-menu
+ * and main sections so they start at the left border.
+ */
+
+#main {
+ margin-left: 0;
+}
+
+.tmtbl-events {
+
+}
+
+/*
+ * Let the top navigation and end selection inputs look alike.
+ */
+#graphwiz-nav, #graphwiz-end-selection {
+ background-color: #c0cbd6;
+ padding-left: 3px;
+ padding-right: 3px;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ margin-left: 1px;
+ margin-right: 1px;
+ margin-top: 3px;
+ margin-bottom: 3px;
+ width: 100%;
+}
+
+
+/*
+ * Navigation and it's inputs.
+ */
+
+#graphwiz-nav {
+ min-height: 4.2em;
+}
+
+#graphwiz-top-1, #graphwiz-top-2 {
+ clear: both;
+}
+
+#graphwiz-time, #graphwiz-top-options-1, #graphwiz-top-submit, #graphwiz-top-options-2 {
+ display: block;
+}
+
+#graphwiz-time, #graphwiz-top-submit {
+ margin-left: 1em;
+ margin-right: 2em;
+ float: left;
+}
+
+#graphwiz-top-options-1, #graphwiz-top-options-2 {
+ margin-left: 2em;
+ margin-right: 1em;
+ float: right;
+}
+
+.graphwiz-pixel-input, .graphwiz-dpi-input, .graphwiz-time-input, .graphwiz-period-input {
+ margin-top: 0.2em;
+ margin-bottom: 0.2em;
+}
+
+.graphwiz-pixel-input {
+ width: 3em;
+ text-align: right
+}
+
+.graphwiz-dpi-input {
+ width: 2em;
+ text-align: right
+}
+
+.graphwiz-time-input {
+ width: 18em;
+ text-align: left
+}
+
+.graphwiz-period-input {
+ width: 4em;
+ text-align: right
+}
+
+.graphwiz-maxerrorbar-input {
+ width: 2em;
+ text-align: right;
+}
+
+.graphwiz-fontsize-input {
+ width: 2em;
+ text-align: right;
+}
+
+.graphwiz-maxpergraph-input {
+ width: 2em;
+ text-align: right;
+}
+
+/*
+ * The graphs.
+ */
+#graphwiz-graphs {
+ margin-top: 0.5em;
+}
+
+.graphwiz-collection {
+ margin-top: 1em;
+ background-color: #f0f0f0;
+ padding-bottom: 1em;
+}
+
+.graphwiz-src-select {
+ margin-left: 0.2em;
+ margin-right: 0.2em;
+ margin-top: 0.2em;
+ margin-bottom: 0.2em;
+ padding-left: 0.3em;
+ padding-top: 0.3em;
+ padding-bottom: 0.3em;
+ padding-right: 0.3em;
+ font-size: 1.4em;
+}
+
+.graphwiz-graph {
+ margin-left: 1em;
+ margin-right: 1em;
+}
+
+.graphwiz-graph svg {
+ width: 100%;
+}
+
+/*
+ * Table data.
+ */
+table.graphwiz-tab {
+ width: auto;
+}
+
+.graphwiz-tab td {
+ text-align: right;
+}
+
+/*
+ * The end selection.
+ */
+#graphwiz-end-selection {
+ margin-top: 1em;
+}
+
+.graphwiz-end-selection-group {
+ clear: both;
+ display: block;
+}
+
+.graphwiz-end-selection-group li {
+ display: block;
+ width: 25%;
+ float: left;
+}
+
+#graphwiz-buildcategories li, #graphwiz-testcase-variations li {
+ width: 50%;
+}
+
+.graphwiz-end-selection-group label {
+ margin-left: 0.3em;
+ vertical-align: middle;
+}
+
+.graphwiz-end-selection-group input {
+ vertical-align: middle;
+}
+
+.graphwiz-end-selection-group h3 {
+ font-size: 1.2em;
+ font-style: italic;
+ font-weight: bold;
+ margin-bottom: 0.26em;
+}
+
+#graphwiz-buildcategories h3, #graphwiz-testcase-variations h3, #graphwiz-end-submit {
+ padding-top: 1em;
+}
+
+#graphwiz-end-submit {
+ clear: both;
+ display: block;
+}
+
+
+
+/*
+ * Tool tip tables.
+ */
+table.graphwiz-tt td:nth-child(1) {
+ font-weight: bold;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css b/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css
new file mode 100644
index 00000000..cb90ae0f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css
@@ -0,0 +1,132 @@
+/* $Id: tooltip.css $ */
+/** @file
+ * Test Manager - Tooltip content (via iframe).
+ */
+
+/*
+ * Copyright (C) 2012-2023 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+ * in the VirtualBox distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+ */
+
+/*
+ * Form the main divs in template-tooltip.html.
+ */
+.tooltip-main {
+ width: 100%;
+}
+
+.tooltip-inner {
+ clear: both;
+ border: 2px solid black;
+ padding-left: 2px;
+ padding-right: 2px;
+ padding-top: 2px;
+ padding-bottom: 2px
+}
+
+/*
+ * Timeline tooltip.
+ */
+.tmtimelinetooltip {
+ font-size: 1em;
+}
+
+/*
+ * Relative stuff that could also be used for a non-tooltip VCS timeline.
+ */
+.tmvcstimeline-highlighted {
+ background: #f0f0f0;
+}
+
+.tmvcstimeline h2 {
+ clear: both;
+ font-size: 120%;
+ background: #e8e8e8;
+ border-bottom: 1px solid #c8c8c8;
+ border-radius: 3px;
+ margin-left: 0.2em;
+ margin-right: 0.2em;
+ margin-top: 0.2em;
+ margin-bottom: 0.4em;
+ padding-left: 0.2em;
+ padding-right: 0.2em;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+}
+
+.tmvcstimeline dl {
+ margin-left: 0.8em;
+ margin-right: 0.2em;
+ margin-top: 0.2em;
+ margin-bottom: 0.8em;
+}
+
+.tmvcstimeline dt {
+ font-size: 118%;
+ padding-left: 0.2em;
+ margin-top: 0.1em;
+ margin-bottom: 0.0em;
+}
+
+.tmvcstimeline dt, .tmvcstimeline :link, .tmvcstimeline :link:visited, .tmvcstimeline :link:hover {
+ color: black;
+ text-decoration: none;
+}
+
+.tmvcstimeline :link:hover {
+ border: 1px dotted black;
+}
+
+.tmvcstimeline-time {
+ font-size: 88%;
+ margin-right: 0.2em;
+}
+
+.tmvcstimeline-time, .tmvcstimeline-author {
+ color: #5858a0;
+}
+
+.tmvcstimeline-rev {
+ color: #0000ee;
+}
+
+.tmvcstimeline dd {
+ padding-left: 2em;
+ margin-top: 0.0em;
+ margin-bottom: 0.4em;
+ color: #424250;
+}
+
+/* This helps highlighting the revision we're showing the tooltip for. */
+.tmvcstimeline-highlight, .tmvcstimeline :target, .tmvcstimeline :target + dd {
+ background-color: #d8e8ff;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg
new file mode 100644
index 00000000..2369828b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg
@@ -0,0 +1,806 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
+<g>
+ <rect fill="none" width="256" height="256"/>
+ <g>
+
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-180.1821" y1="784.8389" x2="-56.9487" y2="784.8389" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#0A2B4D"/>
+ <stop offset="0.0423" style="stop-color:#7DA5DC"/>
+ <stop offset="0.0441" style="stop-color:#83A9DE"/>
+ <stop offset="0.0553" style="stop-color:#A1BFE8"/>
+ <stop offset="0.067" style="stop-color:#B9D0F0"/>
+ <stop offset="0.0794" style="stop-color:#CADCF6"/>
+ <stop offset="0.093" style="stop-color:#D4E4F9"/>
+ <stop offset="0.1099" style="stop-color:#D7E6FA"/>
+ <stop offset="0.254" style="stop-color:#D0DCFA"/>
+ <stop offset="0.42" style="stop-color:#85A0C8"/>
+ <stop offset="0.57" style="stop-color:#2E4573"/>
+ <stop offset="0.6978" style="stop-color:#14335E"/>
+ <stop offset="0.7692" style="stop-color:#1C3866"/>
+ <stop offset="0.8462" style="stop-color:#4F73AA"/>
+ <stop offset="0.9153" style="stop-color:#6487B9"/>
+ <stop offset="1" style="stop-color:#1A3B61"/>
+ </linearGradient>
+ <path fill="url(#SVGID_1_)" d="M129.117,141.783c33.426,0,60.732,23.482,60.732,52.32c0,1.838,0,4.744,0,6.578
+ c0,28.838-27.308,51.803-60.732,51.803c-33.503,0-60.811-22.965-60.811-51.803c0-1.834,0-4.74,0-6.578
+ C68.307,165.266,95.614,141.783,129.117,141.783z"/>
+
+ <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-150.8486" y1="822.7695" x2="-84.4476" y2="740.7712" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#E1E6FA"/>
+ <stop offset="0.07" style="stop-color:#B4C3E1"/>
+ <stop offset="0.11" style="stop-color:#8293B8"/>
+ <stop offset="0.1551" style="stop-color:#2D4173"/>
+ <stop offset="0.2888" style="stop-color:#1B2F61"/>
+ <stop offset="0.5" style="stop-color:#14285A"/>
+ <stop offset="0.7" style="stop-color:#14285A"/>
+ <stop offset="0.8663" style="stop-color:#213970"/>
+ <stop offset="1" style="stop-color:#4164A5"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_2_)" cx="129.041" cy="194.065" rx="57.889" ry="49.1"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638" id="SVGID_3_">
+ <g filter="url(#Adobe_OpacityMaskFilter)">
+
+ <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-85.9404" y1="823.8486" x2="-149.3573" y2="739.6917" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#CCCCCC"/>
+ <stop offset="0.15" style="stop-color:#000000"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_4_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ </g>
+ </mask>
+
+ <radialGradient id="SVGID_5_" cx="-148.8311" cy="824.0654" r="100.2425" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#DCE6FA"/>
+ <stop offset="0.4" style="stop-color:#AAC3EB"/>
+ <stop offset="1" style="stop-color:#AAC3EB"/>
+ </radialGradient>
+ <ellipse mask="url(#SVGID_3_)" fill="url(#SVGID_5_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_1_" filterUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638" id="SVGID_6_">
+ <g filter="url(#Adobe_OpacityMaskFilter_1_)">
+
+ <linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-117.6489" y1="831.0889" x2="-117.6489" y2="732.4512" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#999999"/>
+ <stop offset="0.05" style="stop-color:#000000"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_7_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ </g>
+ </mask>
+
+ <radialGradient id="SVGID_8_" cx="-148.8311" cy="824.0654" r="100.2425" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#DCE6FA"/>
+ <stop offset="0.4" style="stop-color:#AAC3EB"/>
+ <stop offset="1" style="stop-color:#AAC3EB"/>
+ </radialGradient>
+ <ellipse mask="url(#SVGID_6_)" fill="url(#SVGID_8_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_2_" filterUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638" id="SVGID_9_">
+ <g filter="url(#Adobe_OpacityMaskFilter_2_)">
+
+ <linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="-100.396" y1="829.1719" x2="-134.902" y2="734.3676" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#595959"/>
+ <stop offset="0.0535" style="stop-color:#282828"/>
+ <stop offset="0.1" style="stop-color:#000000"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_10_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ </g>
+ </mask>
+
+ <radialGradient id="SVGID_11_" cx="-148.8311" cy="824.0654" r="100.2425" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#DCE6FA"/>
+ <stop offset="0.4" style="stop-color:#AAC3EB"/>
+ <stop offset="1" style="stop-color:#AAC3EB"/>
+ </radialGradient>
+ <ellipse mask="url(#SVGID_9_)" fill="url(#SVGID_11_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_3_" filterUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638" id="SVGID_12_">
+ <g filter="url(#Adobe_OpacityMaskFilter_3_)">
+
+ <linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="-134.9023" y1="829.1719" x2="-100.3965" y2="734.368" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.05" style="stop-color:#1A1A1A"/>
+ <stop offset="0.09" style="stop-color:#000000"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_13_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ </g>
+ </mask>
+
+ <radialGradient id="SVGID_14_" cx="-148.8311" cy="824.0654" r="100.2425" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#DCE6FA"/>
+ <stop offset="0.4" style="stop-color:#AAC3EB"/>
+ <stop offset="1" style="stop-color:#AAC3EB"/>
+ </radialGradient>
+ <ellipse mask="url(#SVGID_12_)" fill="url(#SVGID_14_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+
+ <linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="-178.3818" y1="813.666" x2="-56.8384" y2="813.666" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.0055" style="stop-color:#3C5A78"/>
+ <stop offset="0.033" style="stop-color:#6E8CBE"/>
+ <stop offset="0.123" style="stop-color:#A5BEE1"/>
+ <stop offset="0.25" style="stop-color:#96AFD2"/>
+ <stop offset="0.52" style="stop-color:#3C5A87"/>
+ <stop offset="0.72" style="stop-color:#2B486D"/>
+ <stop offset="0.9" style="stop-color:#466491"/>
+ <stop offset="1" style="stop-color:#3C5082"/>
+ </linearGradient>
+ <path fill="url(#SVGID_15_)" d="M189.852,198.923c0,0.61,0,1.222,0,1.759c0,28.838-27.309,52.318-60.733,52.318
+ c-33.501,0-60.81-23.48-60.81-52.318c0-0.537,0-1.146,0-1.759c0,28.761,27.308,52.243,60.81,52.243
+ C162.543,251.166,189.852,227.684,189.852,198.923z"/>
+
+ <linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="-178.1328" y1="776.4775" x2="-57.1652" y2="787.0609" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.0053" style="stop-color:#9BBEE1"/>
+ <stop offset="0.0802" style="stop-color:#D2E6FA"/>
+ <stop offset="0.1337" style="stop-color:#F0F5FF"/>
+ <stop offset="0.1979" style="stop-color:#F0F5FF"/>
+ <stop offset="0.35" style="stop-color:#DEE6F0"/>
+ <stop offset="0.55" style="stop-color:#AFBEDC"/>
+ <stop offset="0.7" style="stop-color:#96AFD7"/>
+ <stop offset="0.9091" style="stop-color:#A0B4DC"/>
+ <stop offset="1" style="stop-color:#6487AF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_16_)" d="M129.041,141.693c-33.563,0-60.771,23.449-60.771,52.371s27.208,52.371,60.771,52.371
+ c33.564,0,60.771-23.449,60.771-52.371C189.813,165.145,162.604,141.693,129.041,141.693z M129.041,243.165
+ c-31.971,0-57.887-21.983-57.887-49.101c0-27.115,25.916-49.104,57.887-49.104c31.973,0,57.889,21.986,57.889,49.104
+ S161.014,243.165,129.041,243.165z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_4_" filterUnits="userSpaceOnUse" x="68.27" y="141.693" width="121.542" height="104.742">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="68.27" y="141.693" width="121.542" height="104.742" id="SVGID_17_">
+ <g filter="url(#Adobe_OpacityMaskFilter_4_)">
+
+ <linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="-117.6489" y1="834.1406" x2="-117.6489" y2="729.4004" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.35" style="stop-color:#000000"/>
+ <stop offset="0.6" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_18_)" cx="129.041" cy="194.065" rx="60.771" ry="52.37"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_17_)" fill="#142355" d="M129.041,141.693c-33.563,0-60.771,23.449-60.771,52.371
+ s27.208,52.371,60.771,52.371c33.564,0,60.771-23.449,60.771-52.371C189.813,165.145,162.604,141.693,129.041,141.693z
+ M129.041,243.165c-31.971,0-57.887-21.983-57.887-49.101c0-27.115,25.916-49.104,57.887-49.104
+ c31.973,0,57.889,21.986,57.889,49.104S161.014,243.165,129.041,243.165z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_5_" filterUnits="userSpaceOnUse" x="68.27" y="141.693" width="121.542" height="104.742">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="68.27" y="141.693" width="121.542" height="104.742" id="SVGID_19_">
+ <g filter="url(#Adobe_OpacityMaskFilter_5_)">
+
+ <linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="-135.9253" y1="831.9824" x2="-99.3735" y2="731.5574" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.3" style="stop-color:#000000"/>
+ <stop offset="1" style="stop-color:#CCCCCC"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_20_)" cx="129.041" cy="194.065" rx="60.771" ry="52.37"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_19_)" fill="#142355" d="M129.041,141.693c-33.563,0-60.771,23.449-60.771,52.371
+ s27.208,52.371,60.771,52.371c33.564,0,60.771-23.449,60.771-52.371C189.813,165.145,162.604,141.693,129.041,141.693z
+ M129.041,243.165c-31.971,0-57.887-21.983-57.887-49.101c0-27.115,25.916-49.104,57.887-49.104
+ c31.973,0,57.889,21.986,57.889,49.104S161.014,243.165,129.041,243.165z"/>
+
+ <radialGradient id="SVGID_21_" cx="-242.0352" cy="-534.5098" r="111.9119" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#C3D2F0"/>
+ </radialGradient>
+ <path fill="url(#SVGID_21_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699l-92.826,71.046
+ c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045c-1.157-0.889-1.897-2.246-2.011-3.7
+ L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393
+ C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+
+ <linearGradient id="SVGID_22_" gradientUnits="userSpaceOnUse" x1="-121.1333" y1="710.2393" x2="-217.5764" y2="629.314" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.9" style="stop-color:#F0F5FF"/>
+ <stop offset="0.96" style="stop-color:#DCE6F8"/>
+ <stop offset="0.99" style="stop-color:#C3D2F0"/>
+ </linearGradient>
+ <path fill="url(#SVGID_22_)" d="M22.205,49.877c-1.1,1.812,2.384,3.694,2.678,3.872l99.186,60.164
+ c1.081,0.655,1.748,1.842,1.752,3.109l4.361-0.001c0.004-1.266,0.672-2.454,1.752-3.109l-1.959-3.612
+ c-0.578,0.348-1.229,0.521-1.882,0.521c-0.652,0-1.305-0.173-1.883-0.521L26.503,50.528c-0.292-0.175-0.46-0.499-0.439-0.837
+ C26.063,49.691,23.283,48.1,22.205,49.877z"/>
+
+ <linearGradient id="SVGID_23_" gradientUnits="userSpaceOnUse" x1="-106.9102" y1="716.4375" x2="-29.1729" y2="623.7938" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.6" style="stop-color:#F5FAFF"/>
+ <stop offset="0.92" style="stop-color:#E1E6FA"/>
+ <stop offset="0.97" style="stop-color:#CFDAF4"/>
+ <stop offset="1" style="stop-color:#C0CFEE"/>
+ </linearGradient>
+ <path fill="url(#SVGID_23_)" d="M233.797,49.876c1.1,1.812-2.385,3.694-2.678,3.872l-99.188,60.163
+ c-1.08,0.655-1.748,1.842-1.752,3.109l-4.36-0.001c-0.005-1.266-0.672-2.454-1.752-3.109l1.958-3.612
+ c0.579,0.348,1.231,0.521,1.882,0.521c0.652,0,1.304-0.172,1.882-0.521l99.707-59.771c0.291-0.175,0.461-0.499,0.438-0.837
+ C229.938,49.69,232.719,48.099,233.797,49.876z"/>
+
+ <linearGradient id="SVGID_24_" gradientUnits="userSpaceOnUse" x1="-118.689" y1="698.002" x2="-118.689" y2="824.7695" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.9" style="stop-color:#DCE6FF"/>
+ <stop offset="1" style="stop-color:#C2D4F0"/>
+ </linearGradient>
+ <path fill="url(#SVGID_24_)" d="M128,237.064c-2.653,0-2.922-3.642-2.922-3.642c0.138,0,0.278-0.03,0.405-0.096
+ c0.31-0.155,0.507-0.474,0.507-0.819l-0.17-115.49c-0.005-1.267-0.672-2.454-1.752-3.109l2.142-3.612
+ c0.579,0.348,1.231,0.52,1.883,0.52c0.65,0,1.304-0.172,1.882-0.52l1.959,3.611c-1.082,0.655-1.748,1.843-1.752,3.109
+ l-0.17,115.49c0,0.349,0.194,0.665,0.506,0.819c0.127,0.063,0.268,0.095,0.403,0.095C130.922,233.423,130.654,237.064,128,237.064
+ z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_6_" filterUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064" id="SVGID_25_">
+ <g filter="url(#Adobe_OpacityMaskFilter_6_)">
+
+ <linearGradient id="SVGID_26_" gradientUnits="userSpaceOnUse" x1="-225.665" y1="639.6367" x2="-23.757" y2="755.2705" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.78" style="stop-color:#000000"/>
+ <stop offset="0.8" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_26_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699l-92.826,71.046
+ c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045c-1.157-0.889-1.897-2.246-2.011-3.7
+ L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393
+ C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ </g>
+ </mask>
+
+ <linearGradient id="SVGID_27_" gradientUnits="userSpaceOnUse" x1="-213.625" y1="755.2715" x2="-11.7157" y2="639.637" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.2" style="stop-color:#C3D2F0"/>
+ <stop offset="0.22" style="stop-color:#7DA0D2"/>
+ <stop offset="0.2674" style="stop-color:#6E91CD"/>
+ <stop offset="0.7" style="stop-color:#7396D2"/>
+ <stop offset="1" style="stop-color:#5A78B4"/>
+ </linearGradient>
+ <path mask="url(#SVGID_25_)" fill="url(#SVGID_27_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699
+ l-92.826,71.046c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045
+ c-1.157-0.889-1.897-2.246-2.011-3.7L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433
+ c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_7_" filterUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064" id="SVGID_28_">
+ <g filter="url(#Adobe_OpacityMaskFilter_7_)">
+
+ <linearGradient id="SVGID_29_" gradientUnits="userSpaceOnUse" x1="-213.522" y1="755.417" x2="-12.0263" y2="639.0833" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.2" style="stop-color:#FFFFFF"/>
+ <stop offset="0.22" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_29_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699l-92.826,71.046
+ c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045c-1.157-0.889-1.897-2.246-2.011-3.7
+ L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393
+ C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ </g>
+ </mask>
+
+ <linearGradient id="SVGID_30_" gradientUnits="userSpaceOnUse" x1="-225.354" y1="639.084" x2="-23.8602" y2="755.4165" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#B9C8DC"/>
+ <stop offset="0.3" style="stop-color:#D7D5EB"/>
+ <stop offset="0.5" style="stop-color:#D7DCEB"/>
+ <stop offset="0.78" style="stop-color:#DCDCEB"/>
+ <stop offset="0.8" style="stop-color:#C3D2F0"/>
+ </linearGradient>
+ <path mask="url(#SVGID_28_)" fill="url(#SVGID_30_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699
+ l-92.826,71.046c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045
+ c-1.157-0.889-1.897-2.246-2.011-3.7L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433
+ c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_8_" filterUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064" id="SVGID_31_">
+ <g filter="url(#Adobe_OpacityMaskFilter_8_)">
+
+ <radialGradient id="SVGID_32_" cx="-214.9419" cy="621.7891" r="31.5227" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#595959"/>
+ <stop offset="1" style="stop-color:#000000"/>
+ </radialGradient>
+ <path fill="url(#SVGID_32_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699l-92.826,71.046
+ c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045c-1.157-0.889-1.897-2.246-2.011-3.7
+ L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393
+ C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_31_)" fill="#E1F5FF" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699
+ l-92.826,71.046c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045
+ c-1.157-0.889-1.897-2.246-2.011-3.7L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433
+ c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ <g>
+
+ <radialGradient id="SVGID_33_" cx="-118.6724" cy="642.2432" r="64.9423" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#19416E"/>
+ <stop offset="1" style="stop-color:#0A2D64"/>
+ </radialGradient>
+ <path fill="url(#SVGID_33_)" d="M209.039,50.425l-79.438,46.227c-0.477,0.275-1.002,0.412-1.528,0.412
+ c-0.527,0-1.053-0.136-1.528-0.412L46.999,50.426c-0.25-0.145-0.398-0.417-0.384-0.705c0.014-0.288,0.188-0.545,0.449-0.666
+ l79.834-36.784c0.746-0.346,1.604-0.346,2.354,0l79.721,36.783c0.262,0.122,0.435,0.377,0.447,0.667
+ C209.436,50.008,209.287,50.28,209.039,50.425z"/>
+
+ <radialGradient id="SVGID_34_" cx="-172.187" cy="727.8721" r="56.6285" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#19416E"/>
+ <stop offset="1" style="stop-color:#0A2D64"/>
+ </radialGradient>
+ <path fill="url(#SVGID_34_)" d="M114.98,123.905l0.871,83.976c0.002,0.327-0.178,0.628-0.471,0.775
+ c-0.123,0.063-0.257,0.096-0.391,0.096c-0.18,0-0.361-0.057-0.512-0.169l-74.515-54.917c-0.674-0.497-1.095-1.257-1.153-2.092
+ l-5.652-79.07c-0.024-0.324,0.138-0.634,0.417-0.8c0.277-0.166,0.626-0.162,0.902,0.008l79.063,49.616
+ C114.428,121.886,114.969,122.855,114.98,123.905z"/>
+
+ <radialGradient id="SVGID_35_" cx="-1001.7246" cy="782.7354" r="61.9365" gradientTransform="matrix(-0.9143 0 0 0.9143 -734.343 -575.489)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#19416E"/>
+ <stop offset="1" style="stop-color:#0A2D64"/>
+ </radialGradient>
+ <path fill="url(#SVGID_35_)" d="M141.057,123.904l-0.871,83.977c-0.002,0.326,0.179,0.627,0.472,0.775
+ c0.122,0.063,0.258,0.095,0.391,0.095c0.181,0,0.361-0.058,0.513-0.168l74.516-54.917c0.674-0.498,1.096-1.257,1.152-2.093
+ l5.651-79.07c0.022-0.324-0.139-0.634-0.416-0.8c-0.276-0.166-0.627-0.163-0.901,0.008L142.5,121.327
+ C141.609,121.885,141.068,122.854,141.057,123.904z"/>
+ </g>
+
+ <linearGradient id="SVGID_36_" gradientUnits="userSpaceOnUse" x1="-199.5181" y1="619.627" x2="-37.8291" y2="619.627" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#234B82"/>
+ <stop offset="0.489" style="stop-color:#3C5A8C"/>
+ <stop offset="0.5112" style="stop-color:#E6F0FF"/>
+ <stop offset="0.7697" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#F0F5FF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_36_)" d="M208.004,51.354c0.525-0.314,0.885-0.917,0.855-1.54c-0.027-0.646-0.422-1.267-1.006-1.537
+ l-78.097-36.02c-0.534-0.248-1.11-0.371-1.687-0.371s-1.153,0.123-1.688,0.373l-78.2,36.018c-0.586,0.27-1.01,0.89-1.01,1.525
+ c0,0.621,0.328,1.245,0.863,1.557l1.186,0.6c-0.243-0.141-0.387-0.405-0.373-0.686c0.014-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.289,0l77.533,35.772c0.254,0.119,0.422,0.367,0.436,0.649
+ c0.015,0.279-0.131,0.544-0.371,0.685L208.004,51.354z"/>
+
+ <linearGradient id="SVGID_37_" gradientUnits="userSpaceOnUse" x1="-114.0732" y1="786.165" x2="-6.8749" y2="678.9667" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#AFC3D7"/>
+ <stop offset="0.2033" style="stop-color:#BED2E6"/>
+ <stop offset="0.5879" style="stop-color:#BED7EB"/>
+ <stop offset="0.6154" style="stop-color:#3C5A8C"/>
+ <stop offset="1" style="stop-color:#234B82"/>
+ </linearGradient>
+ <path fill="url(#SVGID_37_)" d="M140.545,205.538l-0.002,0.062c-0.002,0.713,0.405,1.371,1.039,1.696
+ c0.271,0.142,0.572,0.212,0.871,0.212c0.393,0,0.795-0.121,1.137-0.375l71.861-52.972c0.945-0.697,1.549-1.777,1.627-2.957
+ l5.451-76.295c0.003-0.051,0.006-0.087,0.006-0.139c-0.003-0.675-0.354-1.291-0.93-1.635c-0.299-0.178-0.64-0.271-0.979-0.271
+ c-0.346,0-0.701,0.094-1.021,0.296l-0.818,0.53c0.266-0.164,0.6-0.167,0.865-0.008c0.266,0.159,0.424,0.457,0.398,0.767
+ l-5.425,75.887c-0.058,0.803-0.461,1.53-1.106,2.008l-71.517,52.707c-0.146,0.108-0.317,0.161-0.489,0.161
+ c-0.13,0-0.259-0.03-0.375-0.091c-0.281-0.143-0.455-0.43-0.453-0.744L140.545,205.538z"/>
+
+ <linearGradient id="SVGID_38_" gradientUnits="userSpaceOnUse" x1="-230.4624" y1="678.9688" x2="-123.2632" y2="786.1679" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#234B82"/>
+ <stop offset="0.3846" style="stop-color:#3C5A8C"/>
+ <stop offset="0.4045" style="stop-color:#F0F5FF"/>
+ <stop offset="0.691" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_38_)" d="M115.354,204.379c0.003,0.314-0.171,0.604-0.451,0.744c-0.119,0.061-0.247,0.091-0.376,0.091
+ c-0.173,0-0.346-0.053-0.49-0.161L42.52,152.346c-0.646-0.478-1.05-1.205-1.106-2.008l-5.425-75.887
+ c-0.023-0.311,0.133-0.608,0.4-0.767c0.266-0.159,0.601-0.156,0.865,0.008l-0.818-0.53c-0.318-0.203-0.675-0.296-1.02-0.296
+ c-0.341,0-0.681,0.092-0.98,0.271c-0.576,0.344-0.926,0.96-0.928,1.635c0,0.052,0.002,0.087,0.004,0.139l5.452,76.295
+ c0.079,1.18,0.682,2.26,1.628,2.957l71.861,52.972c0.341,0.254,0.743,0.375,1.137,0.375c0.298,0,0.599-0.07,0.871-0.212
+ c0.633-0.325,1.04-0.983,1.038-1.696l-0.002-0.062L115.354,204.379z"/>
+
+ <linearGradient id="SVGID_39_" gradientUnits="userSpaceOnUse" x1="-242.0366" y1="-525.2964" x2="-242.0366" y2="-418.9517" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#BEDCFA"/>
+ </linearGradient>
+ <path fill="url(#SVGID_39_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837C229.912,49.351,229.705,49.051,229.395,48.914
+ z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539
+ c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0
+ l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_9_" filterUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344" id="SVGID_40_">
+ <g filter="url(#Adobe_OpacityMaskFilter_9_)">
+
+ <linearGradient id="SVGID_41_" gradientUnits="userSpaceOnUse" x1="-318.0947" y1="-509.2944" x2="-164.9766" y2="-420.8915" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.6" style="stop-color:#000000"/>
+ <stop offset="1" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_41_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837
+ C229.912,49.351,229.705,49.051,229.395,48.914z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4
+ c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649
+ C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_40_)" fill="#C3DCFA" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837C229.912,49.351,229.705,49.051,229.395,48.914
+ z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539
+ c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0
+ l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_10_" filterUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344" id="SVGID_42_">
+ <g filter="url(#Adobe_OpacityMaskFilter_10_)">
+
+ <linearGradient id="SVGID_43_" gradientUnits="userSpaceOnUse" x1="-284.3638" y1="-514.6699" x2="-199.7078" y2="-413.781" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#7A7A7A"/>
+ <stop offset="0.3048" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_43_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837
+ C229.912,49.351,229.705,49.051,229.395,48.914z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4
+ c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649
+ C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ </g>
+ </mask>
+
+ <linearGradient id="SVGID_44_" gradientUnits="userSpaceOnUse" x1="-268.7026" y1="-510.4141" x2="-216.3228" y2="-419.6894" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.0053" style="stop-color:#FFF5F0"/>
+ <stop offset="0.25" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path mask="url(#SVGID_42_)" fill="url(#SVGID_44_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0
+ L26.605,48.914c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771
+ c0.578,0.348,1.231,0.52,1.883,0.52c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837
+ C229.912,49.351,229.705,49.051,229.395,48.914z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4
+ c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649
+ C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_11_" filterUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344" id="SVGID_45_">
+ <g filter="url(#Adobe_OpacityMaskFilter_11_)">
+
+ <linearGradient id="SVGID_46_" gradientUnits="userSpaceOnUse" x1="-302.0186" y1="-413.8936" x2="-182.0531" y2="-514.5565" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#141414"/>
+ <stop offset="0.25" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_46_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837
+ C229.912,49.351,229.705,49.051,229.395,48.914z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4
+ c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649
+ C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_45_)" fill="#2B388F" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837C229.912,49.351,229.705,49.051,229.395,48.914
+ z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539
+ c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0
+ l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+
+ <linearGradient id="SVGID_47_" gradientUnits="userSpaceOnUse" x1="-321.6987" y1="-603.7334" x2="-268.8858" y2="-512.2586" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.0053" style="stop-color:#E6E1E1"/>
+ <stop offset="0.5" style="stop-color:#E3E0DC"/>
+ <stop offset="1" style="stop-color:#EEE9E1"/>
+ </linearGradient>
+ <path fill="url(#SVGID_47_)" d="M125.82,117.02c-0.005-1.267-0.672-2.453-1.752-3.109L24.881,53.748
+ c-0.294-0.178-0.658-0.178-0.951,0.001c-0.292,0.179-0.459,0.505-0.432,0.847l8.144,106.359c0.079,1.038,0.599,1.994,1.429,2.624
+ l91.455,69.657c0.161,0.123,0.357,0.188,0.552,0.188c0.138,0,0.278-0.031,0.405-0.095c0.311-0.155,0.508-0.474,0.507-0.819
+ L125.82,117.02z M113.735,205.9c-0.118,0.061-0.247,0.091-0.375,0.091c-0.174,0-0.347-0.054-0.492-0.161l-71.516-52.707
+ c-0.646-0.479-1.051-1.206-1.107-2.008L34.82,75.227c-0.023-0.311,0.133-0.608,0.4-0.768c0.266-0.159,0.601-0.156,0.865,0.008
+ l75.882,47.62c0.853,0.535,1.372,1.466,1.384,2.473l0.835,80.597C114.19,205.471,114.018,205.758,113.735,205.9z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_12_" filterUnits="userSpaceOnUse" x="23.495" y="53.614" width="102.495" height="179.81">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="23.495" y="53.614" width="102.495" height="179.81" id="SVGID_48_">
+ <g filter="url(#Adobe_OpacityMaskFilter_12_)">
+
+ <linearGradient id="SVGID_49_" gradientUnits="userSpaceOnUse" x1="-218.2163" y1="-602.4976" x2="-372.3692" y2="-513.4973" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.2" style="stop-color:#000000"/>
+ <stop offset="0.9" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_49_)" d="M125.82,117.02c-0.005-1.267-0.672-2.453-1.752-3.109L24.881,53.748
+ c-0.294-0.178-0.658-0.178-0.951,0.001c-0.292,0.179-0.459,0.505-0.432,0.847l8.144,106.359
+ c0.079,1.038,0.599,1.994,1.429,2.624l91.455,69.657c0.161,0.123,0.357,0.188,0.552,0.188c0.138,0,0.278-0.031,0.405-0.095
+ c0.311-0.155,0.508-0.474,0.507-0.819L125.82,117.02z M113.735,205.9c-0.118,0.061-0.247,0.091-0.375,0.091
+ c-0.174,0-0.347-0.054-0.492-0.161l-71.516-52.707c-0.646-0.479-1.051-1.206-1.107-2.008L34.82,75.227
+ c-0.023-0.311,0.133-0.608,0.4-0.768c0.266-0.159,0.601-0.156,0.865,0.008l75.882,47.62c0.853,0.535,1.372,1.466,1.384,2.473
+ l0.835,80.597C114.19,205.471,114.018,205.758,113.735,205.9z"/>
+ </g>
+ </mask>
+
+ <linearGradient id="SVGID_50_" gradientUnits="userSpaceOnUse" x1="-225.4541" y1="-631.8145" x2="-355.862" y2="-476.4005" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#E6D2F0"/>
+ <stop offset="1" style="stop-color:#C3C8DC"/>
+ </linearGradient>
+ <path mask="url(#SVGID_48_)" fill="url(#SVGID_50_)" d="M125.82,117.02c-0.005-1.267-0.672-2.453-1.752-3.109L24.881,53.748
+ c-0.294-0.178-0.658-0.178-0.951,0.001c-0.292,0.179-0.459,0.505-0.432,0.847l8.144,106.359c0.079,1.038,0.599,1.994,1.429,2.624
+ l91.455,69.657c0.161,0.123,0.357,0.188,0.552,0.188c0.138,0,0.278-0.031,0.405-0.095c0.311-0.155,0.508-0.474,0.507-0.819
+ L125.82,117.02z M113.735,205.9c-0.118,0.061-0.247,0.091-0.375,0.091c-0.174,0-0.347-0.054-0.492-0.161l-71.516-52.707
+ c-0.646-0.479-1.051-1.206-1.107-2.008L34.82,75.227c-0.023-0.311,0.133-0.608,0.4-0.768c0.266-0.159,0.601-0.156,0.865,0.008
+ l75.882,47.62c0.853,0.535,1.372,1.466,1.384,2.473l0.835,80.597C114.19,205.471,114.018,205.758,113.735,205.9z"/>
+
+ <linearGradient id="SVGID_51_" gradientUnits="userSpaceOnUse" x1="-162.3706" y1="-603.7349" x2="-215.1844" y2="-512.2589" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#7896BE"/>
+ <stop offset="1" style="stop-color:#4164A5"/>
+ </linearGradient>
+ <path fill="url(#SVGID_51_)" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_13_" filterUnits="userSpaceOnUse" x="130.01" y="53.615" width="102.496" height="179.809">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="130.01" y="53.615" width="102.496" height="179.809" id="SVGID_52_">
+ <g filter="url(#Adobe_OpacityMaskFilter_13_)">
+
+ <linearGradient id="SVGID_53_" gradientUnits="userSpaceOnUse" x1="-148.5908" y1="-581.1997" x2="-238.9791" y2="-529.0141" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.0053" style="stop-color:#808080"/>
+ <stop offset="0.6" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_53_)" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_52_)" fill="#78A0E6" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_14_" filterUnits="userSpaceOnUse" x="130.01" y="53.615" width="102.496" height="179.809">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="130.01" y="53.615" width="102.496" height="179.809" id="SVGID_54_">
+ <g filter="url(#Adobe_OpacityMaskFilter_14_)">
+
+ <linearGradient id="SVGID_55_" gradientUnits="userSpaceOnUse" x1="-210.332" y1="-652.9912" x2="-176.5425" y2="-461.3605" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.0053" style="stop-color:#808080"/>
+ <stop offset="0.4" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_55_)" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_54_)" fill="#7896BE" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ <g id="text_3_">
+ <g>
+ <path fill="#FFFFFF" d="M49.103,122.684c-0.36-0.256-0.717-0.627-1.054-1.077c-0.337-0.45-0.654-0.981-0.936-1.557
+ c-0.281-0.577-0.526-1.199-0.719-1.834c-0.192-0.634-0.332-1.281-0.403-1.907c-0.07-0.625-0.064-1.169,0.006-1.617
+ c0.07-0.449,0.206-0.8,0.394-1.043c0.189-0.242,0.432-0.373,0.718-0.38c0.285-0.008,0.613,0.109,0.973,0.367l4.473,3.189
+ c0.36,0.256,0.718,0.628,1.056,1.08c0.337,0.452,0.657,0.984,0.939,1.562c0.282,0.579,0.528,1.203,0.721,1.84
+ c0.193,0.636,0.334,1.285,0.404,1.911c0.07,0.626,0.064,1.169-0.007,1.617c-0.071,0.447-0.208,0.797-0.398,1.037
+ c-0.19,0.24-0.435,0.369-0.721,0.375c-0.287,0.005-0.616-0.113-0.976-0.371L49.103,122.684 M52.382,114.425l-4.682-3.337
+ c-0.555-0.397-1.063-0.577-1.504-0.567c-0.441,0.012-0.817,0.216-1.109,0.591c-0.293,0.374-0.502,0.919-0.611,1.611
+ c-0.109,0.693-0.117,1.533-0.008,2.5c0.108,0.963,0.325,1.963,0.622,2.943c0.298,0.98,0.677,1.943,1.112,2.835
+ c0.435,0.891,0.926,1.712,1.447,2.409c0.521,0.697,1.073,1.271,1.628,1.667l4.68,3.342c0.557,0.397,1.065,0.581,1.506,0.57
+ c0.442-0.009,0.819-0.212,1.112-0.583c0.293-0.372,0.503-0.915,0.613-1.604c0.11-0.69,0.119-1.529,0.01-2.493
+ c-0.108-0.968-0.324-1.97-0.623-2.954c-0.298-0.983-0.678-1.949-1.115-2.844c-0.435-0.895-0.927-1.717-1.449-2.416
+ C53.492,115.396,52.938,114.821,52.382,114.425 M64.026,122.726l-6.789-4.84l1.575,13.998l1.548,1.105l-1.295-11.521
+ l5.131,3.659c0.18,0.129,0.36,0.314,0.528,0.54c0.169,0.225,0.328,0.491,0.469,0.778c0.141,0.289,0.262,0.6,0.359,0.917
+ c0.095,0.317,0.165,0.642,0.2,0.955c0.034,0.313,0.032,0.585-0.002,0.811c-0.036,0.226-0.103,0.402-0.197,0.522
+ c-0.095,0.123-0.216,0.188-0.359,0.192c-0.143,0.003-0.308-0.055-0.489-0.184l-4.374-3.13l5.418,10.305l2.253,1.607
+ l-3.628-6.758l0.713,0.51c0.376,0.269,0.72,0.393,1.02,0.386c0.299-0.006,0.555-0.142,0.753-0.394
+ c0.199-0.252,0.342-0.619,0.417-1.086c0.075-0.469,0.082-1.035,0.008-1.689c-0.073-0.653-0.219-1.329-0.42-1.994
+ c-0.203-0.666-0.459-1.32-0.755-1.925c-0.295-0.606-0.628-1.164-0.981-1.637C64.775,123.383,64.401,122.994,64.026,122.726
+ M87.361,139.361l-5.595-3.986c-0.558-0.398-1.068-0.581-1.511-0.57c-0.444,0.01-0.823,0.213-1.118,0.588
+ c-0.295,0.373-0.507,0.918-0.619,1.611c-0.111,0.691-0.122,1.536-0.013,2.503c0.109,0.966,0.325,1.968,0.624,2.95
+ c0.298,0.982,0.679,1.949,1.116,2.844c0.438,0.896,0.931,1.719,1.454,2.417c0.523,0.699,1.077,1.274,1.635,1.673l4.802,3.43
+ l0.645-1.813l-5.616-4.009c-0.362-0.258-0.721-0.63-1.059-1.083c-0.339-0.452-0.659-0.984-0.942-1.563
+ c-0.283-0.58-0.529-1.205-0.723-1.84c-0.193-0.639-0.333-1.285-0.403-1.912c-0.07-0.627-0.063-1.173,0.009-1.621
+ c0.072-0.447,0.209-0.802,0.4-1.041c0.192-0.242,0.438-0.373,0.725-0.379c0.288-0.008,0.618,0.112,0.98,0.371l4.571,3.26
+ L87.361,139.361 M89.758,141.07l-1.557-1.109l1.415,12.684c0.01,0.088,0.024,0.177,0.043,0.268
+ c0.018,0.092,0.041,0.184,0.068,0.273c0.028,0.092,0.06,0.186,0.097,0.278c0.037,0.093,0.078,0.187,0.124,0.279
+ c0.044,0.089,0.089,0.174,0.138,0.256c0.048,0.079,0.099,0.153,0.15,0.224c0.052,0.067,0.105,0.131,0.16,0.187
+ c0.054,0.057,0.108,0.103,0.164,0.146l7.112,5.076l0.64-1.816l-7.267-5.188L89.758,141.07 M106.35,152.898l-5.617-4.004
+ c-0.561-0.399-1.073-0.586-1.519-0.576c-0.447,0.011-0.826,0.214-1.123,0.589c-0.297,0.373-0.508,0.919-0.62,1.612
+ c-0.112,0.694-0.123,1.535-0.015,2.506c0.107,0.969,0.324,1.971,0.622,2.953c0.298,0.983,0.679,1.953,1.116,2.849
+ c0.438,0.896,0.932,1.719,1.457,2.421c0.525,0.701,1.081,1.279,1.642,1.679l4.817,3.44l0.643-1.818l-5.633-4.021
+ c-0.298-0.213-0.596-0.506-0.882-0.855c-0.285-0.354-0.561-0.762-0.815-1.215c-0.254-0.451-0.488-0.941-0.691-1.455
+ c-0.203-0.512-0.374-1.046-0.506-1.582l6.679,4.766l0.644-1.817l-7.597-5.419c0.025-0.42,0.098-0.776,0.212-1.063
+ c0.113-0.286,0.268-0.5,0.457-0.635c0.19-0.135,0.412-0.19,0.664-0.161c0.25,0.031,0.529,0.15,0.828,0.362l4.584,3.271
+ L106.35,152.898 M72.648,128.752c-0.1-0.07-0.194-0.119-0.282-0.145c-0.087-0.027-0.169-0.032-0.242-0.018
+ c-0.075,0.016-0.14,0.053-0.198,0.107c-0.058,0.057-0.107,0.132-0.146,0.227l-3.404,9.787l1.829,1.307l2.79-8.179l2.878,7.683
+ l-4.148-2.959l1.189,3.117l4.199,2.998l1.145,3.088l1.831,1.308l-6.404-16.808c-0.068-0.17-0.142-0.33-0.222-0.483
+ c-0.08-0.152-0.166-0.294-0.254-0.425s-0.181-0.248-0.275-0.352C72.838,128.906,72.743,128.82,72.648,128.752"/>
+ </g>
+ <path fill="#FFFFFF" d="M127.647,21.402L64.145,50.701l63.045,36.036l64.158-36.036L127.647,21.402z M127.253,81.504
+ L72.909,50.768l35.185-16.416l8.502,3.661L94.557,59.727l37.996-15.501l-13.538,15.697l24.135-10.007l-12.231,15.239
+ l12.886,7.128L127.253,81.504z M136.936,63.977l18.508-23.021l-23.217,9.484l12.947-15.172L110.25,49.59l12.425-12.426
+ l-10.397-4.773l15.369-7.194l55.001,25.571l-34.4,19.096L136.936,63.977z"/>
+ <g>
+ <path fill="#FFFFFF" d="M155.916,147.47l-1.908,1.384c-0.084,0.539-0.174,1.1-0.269,1.681c-0.099,0.577-0.198,1.18-0.306,1.803
+ c-0.105,0.622-0.222,1.267-0.338,1.929c-0.121,0.664-0.244,1.351-0.373,2.055c-0.134,0.705-0.263,1.388-0.388,2.045
+ c-0.123,0.658-0.246,1.295-0.364,1.908c-0.119,0.612-0.236,1.203-0.351,1.771c-0.111,0.565-0.224,1.112-0.33,1.634
+ c-0.06-0.505-0.114-1.013-0.17-1.523c-0.058-0.514-0.108-1.027-0.162-1.551c-0.053-0.52-0.104-1.045-0.151-1.574
+ c-0.052-0.529-0.099-1.063-0.144-1.604c-0.047-0.541-0.093-1.068-0.133-1.583c-0.039-0.515-0.074-1.015-0.109-1.502
+ c-0.03-0.489-0.062-0.963-0.088-1.424c-0.026-0.463-0.051-0.914-0.069-1.351l-2.175,1.576c0.046,0.646,0.097,1.284,0.146,1.92
+ c0.054,0.633,0.106,1.26,0.162,1.883c0.058,0.623,0.117,1.238,0.179,1.85c0.063,0.609,0.127,1.217,0.194,1.818
+ c0.064,0.599,0.138,1.205,0.209,1.816c0.068,0.612,0.146,1.229,0.228,1.854c0.078,0.625,0.16,1.255,0.246,1.888
+ c0.086,0.637,0.174,1.273,0.267,1.922l2.434-1.775c0.186-0.83,0.365-1.653,0.541-2.469c0.182-0.815,0.354-1.623,0.524-2.426
+ c0.169-0.8,0.337-1.593,0.499-2.375c0.161-0.783,0.319-1.563,0.475-2.33c0.154-0.771,0.309-1.537,0.457-2.308
+ c0.146-0.771,0.293-1.539,0.438-2.31c0.142-0.771,0.281-1.541,0.422-2.313C155.646,149.02,155.783,148.244,155.916,147.47"/>
+ <path fill="#FFFFFF" d="M159.123,149.271l-1.807,1.311l-0.031,0.525l-0.771,12.022l1.805-1.317L159.123,149.271 M158.57,144.455
+ c-0.094,0.064-0.178,0.135-0.256,0.208c-0.08,0.071-0.15,0.149-0.221,0.229c-0.063,0.08-0.125,0.164-0.181,0.25
+ c-0.056,0.088-0.103,0.18-0.146,0.272c-0.041,0.094-0.08,0.194-0.113,0.305c-0.032,0.11-0.063,0.229-0.09,0.354
+ s-0.049,0.259-0.063,0.4c-0.021,0.144-0.032,0.291-0.045,0.451c-0.009,0.162-0.015,0.311-0.015,0.442
+ c0,0.131,0.007,0.25,0.019,0.35c0.013,0.101,0.026,0.188,0.052,0.259c0.021,0.07,0.051,0.127,0.084,0.166
+ c0.032,0.039,0.071,0.065,0.12,0.078c0.05,0.012,0.104,0.012,0.166-0.002c0.062-0.015,0.134-0.042,0.209-0.082
+ c0.076-0.041,0.16-0.094,0.25-0.162c0.095-0.063,0.181-0.135,0.259-0.209c0.08-0.072,0.151-0.149,0.221-0.229
+ c0.066-0.081,0.129-0.164,0.182-0.25c0.055-0.086,0.104-0.178,0.145-0.27c0.041-0.096,0.08-0.197,0.113-0.31
+ c0.031-0.11,0.063-0.229,0.09-0.358c0.025-0.127,0.051-0.267,0.068-0.41c0.016-0.146,0.029-0.299,0.043-0.461
+ c0.01-0.164,0.014-0.311,0.014-0.44c0-0.132-0.008-0.248-0.018-0.349c-0.012-0.102-0.025-0.188-0.047-0.254
+ c-0.021-0.068-0.051-0.123-0.082-0.16c-0.031-0.039-0.072-0.064-0.121-0.076c-0.047-0.012-0.104-0.012-0.166,0.005
+ c-0.063,0.015-0.133,0.043-0.211,0.085C158.75,144.333,158.664,144.386,158.57,144.455"/>
+ <path fill="#FFFFFF" d="M165.184,144.634c-0.069,0.052-0.143,0.113-0.209,0.187c-0.067,0.07-0.139,0.154-0.203,0.248
+ c-0.069,0.095-0.137,0.197-0.203,0.313c-0.063,0.116-0.129,0.242-0.192,0.378c-0.063,0.137-0.135,0.289-0.211,0.463
+ c-0.074,0.174-0.153,0.366-0.24,0.577c-0.086,0.211-0.178,0.443-0.274,0.695c-0.099,0.251-0.199,0.521-0.31,0.813l-0.125-2.01
+ l-1.358,0.987l-0.808,12.532l1.813-1.32l0.516-8.008c0.078-0.208,0.151-0.404,0.223-0.588c0.072-0.184,0.141-0.354,0.203-0.514
+ c0.064-0.158,0.127-0.306,0.184-0.438c0.06-0.137,0.111-0.26,0.162-0.369c0.05-0.105,0.099-0.207,0.146-0.296
+ c0.051-0.089,0.096-0.168,0.145-0.239c0.046-0.068,0.091-0.131,0.136-0.182c0.043-0.053,0.088-0.094,0.131-0.123
+ c0.021-0.016,0.041-0.028,0.065-0.042c0.021-0.015,0.047-0.029,0.07-0.042c0.025-0.014,0.053-0.024,0.08-0.037
+ c0.025-0.014,0.057-0.023,0.086-0.036c0.031-0.008,0.063-0.019,0.092-0.022c0.027-0.012,0.06-0.021,0.092-0.027
+ c0.029-0.008,0.062-0.018,0.091-0.024c0.03-0.008,0.063-0.015,0.094-0.021l0.418-3.135c-0.034,0.011-0.067,0.021-0.101,0.029
+ c-0.03,0.012-0.063,0.021-0.09,0.031c-0.03,0.01-0.061,0.021-0.084,0.033c-0.026,0.011-0.053,0.021-0.076,0.033
+ c-0.022,0.012-0.049,0.021-0.069,0.033s-0.045,0.024-0.067,0.036c-0.021,0.015-0.041,0.026-0.063,0.04
+ C165.223,144.607,165.201,144.621,165.184,144.634"/>
+ <path fill="#FFFFFF" d="M169.717,137.949l-1.076,0.779c-0.07,0.399-0.145,0.791-0.221,1.172c-0.074,0.38-0.154,0.75-0.234,1.11
+ c-0.082,0.361-0.164,0.714-0.252,1.058c-0.084,0.342-0.176,0.676-0.268,0.998l-0.922,0.67l-0.143,2.169l0.91-0.662l-0.435,6.753
+ c-0.022,0.335-0.035,0.639-0.035,0.914c-0.002,0.273,0.006,0.52,0.024,0.736c0.019,0.215,0.05,0.401,0.084,0.563
+ c0.041,0.16,0.089,0.289,0.146,0.393c0.059,0.103,0.133,0.176,0.217,0.225c0.084,0.045,0.182,0.064,0.291,0.056
+ c0.109-0.007,0.23-0.043,0.365-0.104c0.139-0.063,0.283-0.152,0.445-0.271c0.074-0.053,0.15-0.112,0.227-0.177
+ c0.074-0.063,0.152-0.13,0.229-0.204c0.073-0.069,0.153-0.146,0.229-0.229c0.076-0.08,0.156-0.166,0.234-0.254
+ c0.08-0.088,0.154-0.179,0.23-0.271c0.073-0.09,0.146-0.183,0.217-0.274c0.07-0.093,0.139-0.188,0.203-0.281
+ c0.063-0.096,0.129-0.188,0.19-0.286l0.022-1.794c-0.043,0.044-0.086,0.087-0.125,0.125c-0.043,0.039-0.084,0.078-0.123,0.115
+ c-0.041,0.039-0.08,0.074-0.118,0.108c-0.037,0.036-0.078,0.067-0.115,0.101c-0.037,0.03-0.074,0.063-0.108,0.09
+ c-0.041,0.029-0.078,0.062-0.113,0.091c-0.037,0.026-0.074,0.057-0.111,0.087c-0.037,0.025-0.074,0.054-0.111,0.082
+ c-0.055,0.04-0.106,0.068-0.154,0.087c-0.047,0.021-0.092,0.024-0.131,0.021c-0.039-0.008-0.076-0.021-0.104-0.049
+ c-0.033-0.029-0.062-0.068-0.084-0.119s-0.043-0.106-0.062-0.173c-0.014-0.063-0.026-0.137-0.033-0.214
+ c-0.012-0.078-0.014-0.164-0.016-0.258c0-0.094,0.004-0.193,0.01-0.301l0.429-6.604l1.522-1.108l0.142-2.164l-1.523,1.104
+ L169.717,137.949"/>
+ <path fill="#FFFFFF" d="M178.785,134.994l-1.791,1.302l-0.615,9.468c-0.111,0.174-0.225,0.338-0.334,0.487
+ c-0.107,0.149-0.215,0.287-0.32,0.409c-0.104,0.125-0.209,0.234-0.309,0.333c-0.104,0.098-0.201,0.182-0.299,0.252
+ c-0.076,0.057-0.147,0.093-0.213,0.11c-0.063,0.018-0.121,0.014-0.174-0.008s-0.099-0.063-0.14-0.123
+ c-0.04-0.062-0.073-0.142-0.103-0.238c-0.026-0.104-0.049-0.235-0.063-0.402c-0.015-0.167-0.023-0.367-0.025-0.6
+ c-0.002-0.233,0.002-0.498,0.014-0.797s0.027-0.631,0.054-0.996l0.456-6.396l-1.797,1.306c-0.01,0.138-0.021,0.313-0.037,0.531
+ c-0.014,0.219-0.031,0.479-0.055,0.776c-0.021,0.3-0.047,0.64-0.076,1.02c-0.025,0.382-0.061,0.804-0.096,1.265
+ c-0.033,0.462-0.063,0.882-0.092,1.26c-0.025,0.379-0.053,0.716-0.073,1.013c-0.021,0.298-0.04,0.552-0.054,0.767
+ c-0.019,0.215-0.027,0.389-0.037,0.521c-0.031,0.512-0.053,0.979-0.063,1.402c-0.008,0.421-0.002,0.799,0.017,1.131
+ c0.016,0.332,0.045,0.62,0.088,0.863c0.043,0.242,0.1,0.438,0.166,0.595c0.065,0.153,0.146,0.272,0.238,0.356
+ c0.094,0.086,0.196,0.135,0.313,0.149c0.118,0.019,0.245-0.004,0.389-0.056c0.144-0.053,0.298-0.142,0.463-0.264
+ c0.091-0.063,0.181-0.14,0.271-0.224c0.091-0.085,0.185-0.183,0.278-0.285c0.094-0.104,0.188-0.222,0.287-0.348
+ c0.096-0.126,0.194-0.263,0.295-0.409c0.102-0.146,0.196-0.296,0.297-0.452c0.096-0.151,0.188-0.313,0.285-0.479
+ c0.092-0.162,0.186-0.33,0.276-0.504c0.093-0.172,0.185-0.352,0.272-0.533l0.104,1.271l1.389-1.014L178.785,134.994"/>
+ <path fill="#FFFFFF" d="M182.768,141.832c-0.063,0.05-0.127,0.087-0.188,0.115c-0.057,0.027-0.113,0.042-0.162,0.048
+ c-0.053,0.005-0.1,0-0.139-0.017c-0.043-0.019-0.078-0.047-0.113-0.086s-0.063-0.086-0.084-0.145
+ c-0.021-0.061-0.043-0.128-0.055-0.207c-0.014-0.08-0.021-0.168-0.023-0.27c-0.002-0.1,0-0.209,0.008-0.326
+ c0.01-0.143,0.025-0.28,0.051-0.418c0.021-0.137,0.049-0.27,0.086-0.399c0.035-0.131,0.078-0.26,0.125-0.386
+ c0.052-0.126,0.105-0.251,0.17-0.374c0.063-0.123,0.138-0.246,0.224-0.367c0.082-0.119,0.183-0.237,0.285-0.354
+ c0.106-0.115,0.226-0.23,0.354-0.344s0.268-0.224,0.416-0.334l0.521-0.382l-0.166,2.56c-0.059,0.104-0.113,0.203-0.17,0.3
+ c-0.053,0.095-0.109,0.186-0.162,0.271c-0.059,0.085-0.107,0.167-0.162,0.244c-0.052,0.077-0.104,0.146-0.154,0.215
+ c-0.053,0.067-0.104,0.133-0.158,0.191c-0.053,0.063-0.106,0.117-0.162,0.174c-0.055,0.054-0.108,0.104-0.166,0.154
+ C182.885,141.745,182.826,141.791,182.768,141.832 M184.02,130.961c-0.102,0.071-0.199,0.148-0.305,0.232
+ c-0.102,0.084-0.203,0.173-0.311,0.269c-0.105,0.097-0.213,0.196-0.322,0.306c-0.107,0.105-0.219,0.222-0.33,0.34
+ c-0.111,0.117-0.229,0.248-0.354,0.394s-0.254,0.301-0.39,0.472c-0.137,0.172-0.278,0.354-0.426,0.553
+ c-0.147,0.194-0.304,0.406-0.461,0.632l0.067,1.792c0.095-0.117,0.188-0.229,0.277-0.338c0.092-0.108,0.18-0.213,0.27-0.313
+ c0.089-0.1,0.173-0.196,0.257-0.288c0.084-0.091,0.161-0.179,0.241-0.261c0.08-0.083,0.158-0.164,0.24-0.242
+ c0.08-0.079,0.164-0.153,0.246-0.228c0.084-0.071,0.168-0.146,0.254-0.213c0.086-0.068,0.174-0.135,0.262-0.198
+ c0.099-0.072,0.191-0.132,0.281-0.181c0.084-0.047,0.168-0.082,0.246-0.104c0.076-0.022,0.148-0.033,0.215-0.031
+ c0.067,0.002,0.127,0.017,0.185,0.044c0.055,0.026,0.103,0.068,0.142,0.127c0.036,0.058,0.069,0.133,0.092,0.226
+ c0.023,0.092,0.035,0.198,0.041,0.323c0.008,0.123,0.006,0.266-0.006,0.422l-0.056,0.845l-0.483,0.353
+ c-0.295,0.215-0.57,0.439-0.832,0.676c-0.256,0.236-0.5,0.481-0.724,0.74c-0.224,0.258-0.43,0.524-0.618,0.805
+ c-0.187,0.277-0.355,0.565-0.513,0.867c-0.151,0.301-0.287,0.604-0.406,0.907c-0.12,0.306-0.223,0.611-0.312,0.919
+ c-0.086,0.309-0.153,0.615-0.209,0.926c-0.055,0.312-0.092,0.623-0.11,0.935c-0.039,0.601-0.028,1.093,0.031,1.479
+ c0.057,0.385,0.166,0.662,0.321,0.832c0.156,0.17,0.365,0.233,0.621,0.189c0.252-0.043,0.558-0.193,0.908-0.452
+ c0.098-0.067,0.189-0.147,0.287-0.235c0.094-0.088,0.188-0.187,0.283-0.293c0.098-0.106,0.189-0.225,0.284-0.349
+ c0.099-0.125,0.19-0.259,0.288-0.401c0.096-0.146,0.188-0.293,0.281-0.445c0.092-0.15,0.181-0.305,0.268-0.463
+ c0.086-0.157,0.17-0.318,0.252-0.481c0.082-0.164,0.16-0.33,0.236-0.498l0.104,1.269l1.311-0.957l0.566-8.646
+ c0.021-0.337,0.029-0.645,0.021-0.921c-0.002-0.276-0.021-0.524-0.053-0.742c-0.035-0.217-0.08-0.403-0.14-0.561
+ c-0.062-0.157-0.133-0.283-0.22-0.38c-0.086-0.099-0.19-0.161-0.309-0.195c-0.119-0.033-0.254-0.036-0.404-0.005
+ c-0.149,0.028-0.317,0.092-0.5,0.185C184.432,130.682,184.232,130.807,184.02,130.961"/>
+ <path fill="#FFFFFF" d="M191.021,121.232l-1.761,1.275l-0.987,15.075c-0.019,0.257-0.023,0.488-0.023,0.693
+ s0.011,0.387,0.025,0.543c0.02,0.156,0.045,0.287,0.078,0.393s0.074,0.188,0.127,0.246c0.051,0.057,0.108,0.093,0.18,0.113
+ c0.068,0.02,0.146,0.021,0.234,0.004c0.088-0.016,0.184-0.053,0.289-0.105c0.104-0.052,0.221-0.125,0.344-0.215
+ c0.104-0.077,0.212-0.162,0.32-0.256c0.106-0.094,0.219-0.196,0.33-0.307c0.112-0.11,0.227-0.23,0.342-0.358
+ c0.116-0.128,0.229-0.265,0.352-0.409l0.027-1.81l-0.426,0.332c-0.046,0.032-0.082,0.056-0.119,0.069s-0.07,0.018-0.099,0.012
+ c-0.028-0.004-0.053-0.019-0.073-0.041c-0.021-0.022-0.041-0.055-0.056-0.094c-0.015-0.041-0.022-0.099-0.03-0.168
+ c-0.007-0.07-0.013-0.156-0.015-0.259c-0.002-0.101,0-0.217,0.006-0.347c0.002-0.131,0.009-0.277,0.021-0.438L191.021,121.232"
+ />
+ <path fill="#FFFFFF" d="M195.492,132.34l-0.818,0.523l0.33-5.009l0.77-0.559c0.14-0.101,0.269-0.181,0.39-0.24
+ c0.121-0.06,0.229-0.098,0.332-0.114c0.101-0.017,0.19-0.013,0.271,0.012c0.084,0.024,0.152,0.072,0.215,0.139
+ c0.062,0.069,0.115,0.148,0.158,0.243c0.041,0.094,0.076,0.201,0.104,0.322c0.023,0.12,0.041,0.254,0.047,0.402
+ c0.006,0.149,0.006,0.31-0.006,0.485c-0.016,0.205-0.035,0.403-0.063,0.597c-0.029,0.192-0.063,0.379-0.107,0.563
+ c-0.045,0.185-0.094,0.36-0.153,0.534c-0.058,0.173-0.119,0.342-0.194,0.506c-0.07,0.164-0.151,0.318-0.242,0.469
+ c-0.088,0.148-0.187,0.289-0.293,0.422c-0.105,0.135-0.219,0.26-0.342,0.376C195.764,132.129,195.633,132.238,195.492,132.34
+ M195.164,125.433l0.277-4.249l0.852-0.618c0.098-0.069,0.186-0.122,0.27-0.158c0.082-0.036,0.158-0.056,0.228-0.06
+ c0.069-0.004,0.133,0.008,0.188,0.037c0.059,0.028,0.106,0.071,0.149,0.131c0.041,0.062,0.078,0.134,0.104,0.219
+ c0.029,0.085,0.056,0.18,0.068,0.287c0.018,0.107,0.027,0.226,0.029,0.355c0.004,0.129,0,0.27-0.012,0.423
+ c-0.01,0.155-0.025,0.309-0.051,0.461c-0.023,0.153-0.058,0.305-0.095,0.457c-0.038,0.15-0.084,0.301-0.137,0.45
+ c-0.054,0.149-0.115,0.298-0.183,0.445c-0.067,0.146-0.143,0.286-0.222,0.418c-0.081,0.132-0.167,0.257-0.261,0.374
+ c-0.094,0.116-0.191,0.225-0.299,0.326c-0.105,0.101-0.219,0.195-0.338,0.282L195.164,125.433 M196.744,117.89l-2.961,2.146
+ l-1.102,16.726l2.411-1.762c0.334-0.243,0.646-0.501,0.938-0.773c0.292-0.271,0.563-0.559,0.813-0.858
+ c0.25-0.302,0.479-0.617,0.688-0.948c0.206-0.331,0.395-0.676,0.561-1.037c0.164-0.357,0.313-0.72,0.443-1.086
+ c0.131-0.363,0.244-0.732,0.34-1.105c0.097-0.373,0.174-0.75,0.233-1.129c0.062-0.379,0.104-0.763,0.13-1.15
+ c0.014-0.209,0.018-0.409,0.01-0.598c-0.008-0.19-0.026-0.368-0.06-0.537c-0.026-0.168-0.067-0.326-0.119-0.474
+ c-0.053-0.147-0.114-0.285-0.188-0.412c-0.069-0.127-0.149-0.23-0.233-0.312c-0.086-0.082-0.176-0.14-0.275-0.176
+ c-0.1-0.036-0.201-0.05-0.313-0.04c-0.111,0.009-0.229,0.042-0.353,0.097c0.119-0.177,0.229-0.354,0.334-0.531
+ c0.104-0.177,0.201-0.353,0.291-0.53c0.09-0.177,0.172-0.354,0.248-0.533c0.075-0.177,0.146-0.356,0.207-0.535
+ c0.062-0.176,0.117-0.36,0.164-0.551c0.053-0.19,0.096-0.388,0.135-0.592c0.037-0.203,0.069-0.413,0.101-0.63
+ c0.024-0.217,0.045-0.438,0.063-0.668c0.021-0.313,0.023-0.598,0.016-0.856c-0.014-0.258-0.037-0.487-0.08-0.69
+ c-0.041-0.203-0.1-0.377-0.172-0.526c-0.074-0.148-0.164-0.269-0.268-0.362c-0.104-0.092-0.225-0.15-0.359-0.179
+ c-0.137-0.027-0.287-0.024-0.453,0.012s-0.348,0.104-0.545,0.205C197.188,117.591,196.973,117.724,196.744,117.89"/>
+ <path fill="#FFFFFF" d="M203.377,126.843c-0.111,0.082-0.217,0.139-0.311,0.17c-0.096,0.033-0.181,0.041-0.26,0.024
+ c-0.076-0.017-0.146-0.058-0.207-0.124c-0.063-0.066-0.115-0.157-0.16-0.273c-0.043-0.117-0.078-0.271-0.104-0.46
+ c-0.025-0.19-0.043-0.417-0.053-0.681c-0.008-0.263-0.008-0.564,0.002-0.902c0.01-0.337,0.027-0.712,0.057-1.124
+ c0.021-0.324,0.047-0.631,0.08-0.92c0.031-0.29,0.07-0.562,0.113-0.817c0.045-0.255,0.096-0.495,0.15-0.717
+ c0.057-0.221,0.115-0.425,0.184-0.614c0.064-0.188,0.137-0.358,0.213-0.517c0.076-0.159,0.158-0.305,0.246-0.438
+ c0.086-0.131,0.176-0.249,0.271-0.354c0.1-0.105,0.198-0.196,0.309-0.274c0.104-0.075,0.197-0.127,0.287-0.154
+ c0.092-0.026,0.172-0.028,0.246-0.005c0.072,0.023,0.139,0.071,0.197,0.145c0.057,0.072,0.106,0.17,0.149,0.293
+ c0.043,0.124,0.076,0.282,0.103,0.477c0.022,0.195,0.041,0.426,0.047,0.693c0.01,0.267,0.008,0.569-0.002,0.908
+ c-0.011,0.339-0.027,0.713-0.056,1.125c-0.022,0.323-0.049,0.629-0.081,0.916c-0.031,0.288-0.07,0.558-0.113,0.81
+ c-0.043,0.251-0.09,0.485-0.146,0.7c-0.054,0.217-0.113,0.414-0.179,0.593c-0.063,0.182-0.135,0.35-0.209,0.503
+ c-0.071,0.154-0.149,0.293-0.231,0.419c-0.082,0.125-0.17,0.239-0.26,0.338C203.57,126.685,203.477,126.771,203.377,126.843
+ M204.135,116.359c-0.248,0.18-0.484,0.385-0.709,0.616c-0.229,0.231-0.443,0.488-0.646,0.771
+ c-0.204,0.284-0.397,0.593-0.581,0.929c-0.185,0.335-0.357,0.698-0.521,1.087c-0.161,0.386-0.308,0.792-0.438,1.22
+ c-0.13,0.428-0.244,0.876-0.345,1.345c-0.1,0.469-0.186,0.958-0.25,1.469c-0.065,0.511-0.121,1.041-0.156,1.592
+ c-0.034,0.547-0.053,1.044-0.053,1.49c0,0.447,0.019,0.845,0.056,1.191c0.036,0.347,0.092,0.644,0.165,0.892
+ c0.074,0.247,0.164,0.444,0.275,0.594c0.109,0.146,0.238,0.254,0.383,0.318c0.146,0.066,0.308,0.09,0.484,0.074
+ c0.18-0.016,0.375-0.074,0.59-0.172c0.213-0.1,0.441-0.238,0.689-0.418c0.25-0.185,0.488-0.394,0.717-0.627
+ c0.229-0.234,0.447-0.498,0.652-0.785c0.207-0.287,0.402-0.6,0.59-0.94c0.186-0.34,0.359-0.707,0.523-1.101
+ c0.164-0.393,0.313-0.806,0.444-1.237c0.136-0.433,0.248-0.884,0.351-1.355c0.1-0.472,0.184-0.962,0.25-1.472
+ c0.067-0.511,0.123-1.041,0.158-1.59c0.034-0.516,0.049-0.986,0.047-1.412c-0.004-0.426-0.023-0.807-0.064-1.143
+ c-0.039-0.335-0.1-0.625-0.178-0.871c-0.076-0.246-0.174-0.445-0.287-0.6c-0.113-0.155-0.244-0.27-0.393-0.341
+ c-0.146-0.073-0.31-0.103-0.488-0.09c-0.18,0.01-0.375,0.063-0.584,0.157C204.604,116.047,204.377,116.183,204.135,116.359"/>
+ <path fill="#FFFFFF" d="M213.489,109.8l-1.823,1.325c-0.043,0.158-0.084,0.317-0.127,0.478c-0.043,0.16-0.084,0.321-0.129,0.482
+ c-0.043,0.162-0.088,0.324-0.133,0.487c-0.047,0.162-0.094,0.326-0.139,0.49c-0.046,0.163-0.091,0.322-0.138,0.478
+ c-0.045,0.156-0.09,0.31-0.135,0.458c-0.046,0.148-0.091,0.292-0.136,0.433s-0.086,0.277-0.129,0.409l-0.664-2.463l-2.035,1.479
+ l1.427,4.602l-2.521,7.942l1.875-1.37c0.053-0.206,0.105-0.413,0.16-0.619c0.053-0.207,0.107-0.414,0.162-0.621
+ c0.059-0.208,0.113-0.417,0.17-0.626c0.061-0.209,0.117-0.419,0.178-0.631c0.06-0.211,0.117-0.419,0.177-0.623
+ c0.059-0.204,0.116-0.404,0.176-0.601c0.06-0.196,0.116-0.389,0.174-0.579c0.058-0.189,0.115-0.375,0.172-0.558l0.857,3.234
+ l2.096-1.531l-1.696-5.256L213.489,109.8"/>
+ </g>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png
new file mode 100644
index 00000000..d8849bdd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png
Binary files differ
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico b/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico
new file mode 100644
index 00000000..72f7032d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico
Binary files differ
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup b/src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/common.js b/src/VBox/ValidationKit/testmanager/htdocs/js/common.js
new file mode 100644
index 00000000..52c4179c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/js/common.js
@@ -0,0 +1,1926 @@
+/* $Id: common.js $ */
+/** @file
+ * Common JavaScript functions
+ */
+
+/*
+ * Copyright (C) 2012-2023 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+ * in the VirtualBox distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+ */
+
+
+/*********************************************************************************************************************************
+* Global Variables *
+*********************************************************************************************************************************/
+/** Same as WuiDispatcherBase.ksParamRedirectTo. */
+var g_ksParamRedirectTo = 'RedirectTo';
+
+/** Days of the week in Date() style with Sunday first. */
+var g_kasDaysOfTheWeek = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ];
+
+
+/**
+ * Detects the firefox browser.
+ */
+function isBrowserFirefox()
+{
+ return typeof InstallTrigger !== 'undefined';
+}
+
+/**
+ * Detects the google chrome browser.
+ * @note Might be confused with edge chromium
+ */
+function isBrowserChrome()
+{
+ var oChrome = window.chrome;
+ if (!oChrome)
+ return false;
+ return !!oChrome.runtime || !oChrome.webstore;
+}
+
+/**
+ * Detects the chromium-based edge browser.
+ */
+function isBrowserEdgeChromium()
+{
+ if (!isBrowserChrome())
+ return false;
+ return navigation.userAgent.indexOf('Edg') >= 0
+}
+
+/**
+ * Detects the chromium-based edge browser.
+ */
+function isBrowserInternetExplorer()
+{
+ /* documentMode is an IE only property. Values are 5,7,8,9,10 or 11
+ according to google results. */
+ if (typeof document.documentMode !== 'undefined')
+ {
+ if (document.documentMode)
+ return true;
+ }
+ /* IE only conditional compiling feature. Here, the 'true || ' part
+ will be included in the if when executing in IE: */
+ if (/*@cc_on true || @*/false)
+ return true;
+ return false;
+}
+
+/**
+ * Detects the safari browser (v3+).
+ */
+function isBrowserSafari()
+{
+ /* Check if window.HTMLElement is a function named 'HTMLElementConstructor()'?
+ Should work for older safari versions. */
+ var sStr = window.HTMLElement.toString();
+ if (/constructor/i.test(sStr))
+ return true;
+
+ /* Check the class name of window.safari.pushNotification. This works for current. */
+ var oSafari = window['safari'];
+ if (oSafari)
+ {
+ if (typeof oSafari !== 'undefined')
+ {
+ var oPushNotify = oSafari.pushNotification;
+ if (oPushNotify)
+ {
+ sStr = oPushNotify.toString();
+ if (/\[object Safari.*Notification\]/.test(sStr))
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Checks if the given value is a decimal integer value.
+ *
+ * @returns true if it is, false if it's isn't.
+ * @param sValue The value to inspect.
+ */
+function isInteger(sValue)
+{
+ if (typeof sValue != 'undefined')
+ {
+ var intRegex = /^\d+$/;
+ if (intRegex.test(sValue))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Checks if @a oMemmber is present in aoArray.
+ *
+ * @returns true/false.
+ * @param aoArray The array to check.
+ * @param oMember The member to check for.
+ */
+function isMemberOfArray(aoArray, oMember)
+{
+ var i;
+ for (i = 0; i < aoArray.length; i++)
+ if (aoArray[i] == oMember)
+ return true;
+ return false;
+}
+
+/**
+ * Parses a typical ISO timestamp, returing a Date object, reasonably
+ * forgiving, but will throw weird indexing/conversion errors if the input
+ * is malformed.
+ *
+ * @returns Date object.
+ * @param sTs The timestamp to parse.
+ * @sa parseIsoTimestamp() in utils.py.
+ */
+function parseIsoTimestamp(sTs)
+{
+ /* YYYY-MM-DD */
+ var iYear = parseInt(sTs.substring(0, 4), 10);
+ console.assert(sTs.charAt(4) == '-');
+ var iMonth = parseInt(sTs.substring(5, 7), 10);
+ console.assert(sTs.charAt(7) == '-');
+ var iDay = parseInt(sTs.substring(8, 10), 10);
+
+ /* Skip separator */
+ var sTime = sTs.substring(10);
+ while ('Tt \t\n\r'.includes(sTime.charAt(0))) {
+ sTime = sTime.substring(1);
+ }
+
+ /* HH:MM[:SS[.fraction] */
+ var iHour = parseInt(sTime.substring(0, 2), 10);
+ console.assert(sTime.charAt(2) == ':');
+ var iMin = parseInt(sTime.substring(3, 5), 10);
+ var iSec = 0;
+ var iMicroseconds = 0;
+ var offTime = 5;
+ if (sTime.charAt(5) == ':')
+ {
+ iSec = parseInt(sTime.substring(6, 8), 10);
+
+ /* Fraction? */
+ offTime = 8;
+ if (offTime < sTime.length && '.,'.includes(sTime.charAt(offTime)))
+ {
+ offTime += 1;
+ var cchFraction = 0;
+ while (offTime + cchFraction < sTime.length && '0123456789'.includes(sTime.charAt(offTime + cchFraction)))
+ cchFraction += 1;
+ if (cchFraction > 0)
+ {
+ iMicroseconds = parseInt(sTime.substring(offTime, offTime + cchFraction), 10);
+ offTime += cchFraction;
+ while (cchFraction < 6)
+ {
+ iMicroseconds *= 10;
+ cchFraction += 1;
+ }
+ while (cchFraction > 6)
+ {
+ iMicroseconds = iMicroseconds / 10;
+ cchFraction -= 1;
+ }
+ }
+ }
+ }
+ var iMilliseconds = (iMicroseconds + 499) / 1000;
+
+ /* Naive? */
+ var oDate = new Date(Date.UTC(iYear, iMonth - 1, iDay, iHour, iMin, iSec, iMilliseconds));
+ if (offTime >= sTime.length)
+ return oDate;
+
+ /* Zulu? */
+ if (offTime >= sTime.length || 'Zz'.includes(sTime.charAt(offTime)))
+ return oDate;
+
+ /* Some kind of offset afterwards. */
+ var chSign = sTime.charAt(offTime);
+ if ('+-'.includes(chSign))
+ {
+ offTime += 1;
+ var cMinTz = parseInt(sTime.substring(offTime, offTime + 2), 10) * 60;
+ offTime += 2;
+ if (offTime < sTime.length && sTime.charAt(offTime) == ':')
+ offTime += 1;
+ if (offTime + 2 <= sTime.length)
+ {
+ cMinTz += parseInt(sTime.substring(offTime, offTime + 2), 10);
+ offTime += 2;
+ }
+ console.assert(offTime == sTime.length);
+ if (chSign == '-')
+ cMinTz = -cMinTz;
+
+ return new Date(oDate.getTime() - cMinTz * 60000);
+ }
+ console.assert(false);
+ return oDate;
+}
+
+/**
+ * @param oDate Date object.
+ */
+function formatTimeHHMM(oDate, fNbsp)
+{
+ var sTime = oDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit'} );
+ if (fNbsp === true)
+ sTime = sTime.replace(' ', '\u00a0');
+
+ /* Workaround for single digit hours in firefox with en_US (minutes works fine): */
+ var iHours = oDate.getHours();
+ if ((iHours % 12) < 10)
+ {
+ var ch1 = sTime.substr(0, 1);
+ var ch2 = sTime.substr(1, 1);
+ if ( ch1 == (iHours % 12).toString()
+ && !(ch2 >= '0' && ch2 <= '9'))
+ sTime = '0' + sTime;
+ }
+ return sTime;
+}
+
+/**
+ * Escapes special characters to HTML-safe sequences, for element use.
+ *
+ * @returns Escaped string suitable for HTML.
+ * @param sText Plain text to escape.
+ */
+function escapeElem(sText)
+{
+ sText = sText.replace(/&/g, '&amp;');
+ sText = sText.replace(/>/g, '&lt;');
+ return sText.replace(/</g, '&gt;');
+}
+
+/**
+ * Escapes special characters to HTML-safe sequences, for double quoted
+ * attribute use.
+ *
+ * @returns Escaped string suitable for HTML.
+ * @param sText Plain text to escape.
+ */
+function escapeAttr(sText)
+{
+ sText = sText.replace(/&/g, '&amp;');
+ sText = sText.replace(/</g, '&lt;');
+ sText = sText.replace(/>/g, '&gt;');
+ return sText.replace(/"/g, '&quot;');
+}
+
+/**
+ * Removes the element with the specified ID.
+ */
+function removeHtmlNode(sContainerId)
+{
+ var oElement = document.getElementById(sContainerId);
+ if (oElement)
+ {
+ oElement.parentNode.removeChild(oElement);
+ }
+}
+
+/**
+ * Sets the value of the element with id @a sInputId to the keys of aoItems
+ * (comma separated).
+ */
+function setElementValueToKeyList(sInputId, aoItems)
+{
+ var sKey;
+ var oElement = document.getElementById(sInputId);
+ oElement.value = '';
+
+ for (sKey in aoItems)
+ {
+ if (oElement.value.length > 0)
+ {
+ oElement.value += ',';
+ }
+
+ oElement.value += sKey;
+ }
+}
+
+/**
+ * Get the Window.devicePixelRatio in a safe way.
+ *
+ * @returns Floating point ratio. 1.0 means it's a 1:1 ratio.
+ */
+function getDevicePixelRatio()
+{
+ var fpRatio = 1.0;
+ if (window.devicePixelRatio)
+ {
+ fpRatio = window.devicePixelRatio;
+ if (fpRatio < 0.5 || fpRatio > 10.0)
+ fpRatio = 1.0;
+ }
+ return fpRatio;
+}
+
+/**
+ * Tries to figure out the DPI of the device in the X direction.
+ *
+ * @returns DPI on success, null on failure.
+ */
+function getDeviceXDotsPerInch()
+{
+ if (window.deviceXDPI && window.deviceXDPI > 48 && window.deviceXDPI < 2048)
+ {
+ return window.deviceXDPI;
+ }
+ else if (window.devicePixelRatio && window.devicePixelRatio >= 0.5 && window.devicePixelRatio <= 10.0)
+ {
+ cDotsPerInch = Math.round(96 * window.devicePixelRatio);
+ }
+ else
+ {
+ cDotsPerInch = null;
+ }
+ return cDotsPerInch;
+}
+
+/**
+ * Gets the width of the given element (downscaled).
+ *
+ * Useful when using the element to figure the size of a image
+ * or similar.
+ *
+ * @returns Number of pixels. null if oElement is bad.
+ * @param oElement The element (not ID).
+ */
+function getElementWidth(oElement)
+{
+ if (oElement && oElement.offsetWidth)
+ return oElement.offsetWidth;
+ return null;
+}
+
+/** By element ID version of getElementWidth. */
+function getElementWidthById(sElementId)
+{
+ return getElementWidth(document.getElementById(sElementId));
+}
+
+/**
+ * Gets the real unscaled width of the given element.
+ *
+ * Useful when using the element to figure the size of a image
+ * or similar.
+ *
+ * @returns Number of screen pixels. null if oElement is bad.
+ * @param oElement The element (not ID).
+ */
+function getUnscaledElementWidth(oElement)
+{
+ if (oElement && oElement.offsetWidth)
+ return Math.round(oElement.offsetWidth * getDevicePixelRatio());
+ return null;
+}
+
+/** By element ID version of getUnscaledElementWidth. */
+function getUnscaledElementWidthById(sElementId)
+{
+ return getUnscaledElementWidth(document.getElementById(sElementId));
+}
+
+/**
+ * Gets the part of the URL needed for a RedirectTo parameter.
+ *
+ * @returns URL string.
+ */
+function getCurrentBrowerUrlPartForRedirectTo()
+{
+ var sWhere = window.location.href;
+ var offTmp;
+ var offPathKeep;
+
+ /* Find the end of that URL 'path' component. */
+ var offPathEnd = sWhere.indexOf('?');
+ if (offPathEnd < 0)
+ offPathEnd = sWhere.indexOf('#');
+ if (offPathEnd < 0)
+ offPathEnd = sWhere.length;
+
+ /* Go backwards from the end of the and find the start of the last component. */
+ offPathKeep = sWhere.lastIndexOf("/", offPathEnd);
+ offTmp = sWhere.lastIndexOf(":", offPathEnd);
+ if (offPathKeep < offTmp)
+ offPathKeep = offTmp;
+ offTmp = sWhere.lastIndexOf("\\", offPathEnd);
+ if (offPathKeep < offTmp)
+ offPathKeep = offTmp;
+
+ return sWhere.substring(offPathKeep + 1);
+}
+
+/**
+ * Adds the given sorting options to the URL and reloads.
+ *
+ * This will preserve previous sorting columns except for those
+ * given in @a aiColumns.
+ *
+ * @param sParam Sorting parameter.
+ * @param aiColumns Array of sorting columns.
+ */
+function ahrefActionSortByColumns(sParam, aiColumns)
+{
+ var sWhere = window.location.href;
+
+ var offHash = sWhere.indexOf('#');
+ if (offHash < 0)
+ offHash = sWhere.length;
+
+ var offQm = sWhere.indexOf('?');
+ if (offQm > offHash)
+ offQm = -1;
+
+ var sNew = '';
+ if (offQm > 0)
+ sNew = sWhere.substring(0, offQm);
+
+ sNew += '?' + sParam + '=' + aiColumns[0];
+ var i;
+ for (i = 1; i < aiColumns.length; i++)
+ sNew += '&' + sParam + '=' + aiColumns[i];
+
+ if (offQm >= 0 && offQm + 1 < offHash)
+ {
+ var sArgs = '&' + sWhere.substring(offQm + 1, offHash);
+ var off = 0;
+ while (off < sArgs.length)
+ {
+ var offMatch = sArgs.indexOf('&' + sParam + '=', off);
+ if (offMatch >= 0)
+ {
+ if (off < offMatch)
+ sNew += sArgs.substring(off, offMatch);
+
+ var offValue = offMatch + 1 + sParam.length + 1;
+ offEnd = sArgs.indexOf('&', offValue);
+ if (offEnd < offValue)
+ offEnd = sArgs.length;
+
+ var iColumn = parseInt(sArgs.substring(offValue, offEnd));
+ if (!isMemberOfArray(aiColumns, iColumn) && !isMemberOfArray(aiColumns, -iColumn))
+ sNew += sArgs.substring(offMatch, offEnd);
+
+ off = offEnd;
+ }
+ else
+ {
+ sNew += sArgs.substring(off);
+ break;
+ }
+ }
+ }
+
+ if (offHash < sWhere.length)
+ sNew = sWhere.substr(offHash);
+
+ window.location.href = sNew;
+}
+
+/**
+ * Sets the value of an input field element (give by ID).
+ *
+ * @returns Returns success indicator (true/false).
+ * @param sFieldId The field ID (required for updating).
+ * @param sValue The field value.
+ */
+function setInputFieldValue(sFieldId, sValue)
+{
+ var oInputElement = document.getElementById(sFieldId);
+ if (oInputElement)
+ {
+ oInputElement.value = sValue;
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Adds a hidden input field to a form.
+ *
+ * @returns The new input field element.
+ * @param oFormElement The form to append it to.
+ * @param sName The field name.
+ * @param sValue The field value.
+ * @param sFieldId The field ID (optional).
+ */
+function addHiddenInputFieldToForm(oFormElement, sName, sValue, sFieldId)
+{
+ var oNew = document.createElement('input');
+ oNew.type = 'hidden';
+ oNew.name = sName;
+ oNew.value = sValue;
+ if (sFieldId)
+ oNew.id = sFieldId;
+ oFormElement.appendChild(oNew);
+ return oNew;
+}
+
+/** By element ID version of addHiddenInputFieldToForm. */
+function addHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId)
+{
+ return addHiddenInputFieldToForm(document.getElementById(sFormId), sName, sValue, sFieldId);
+}
+
+/**
+ * Adds or updates a hidden input field to/on a form.
+ *
+ * @returns The new input field element.
+ * @param sFormId The ID of the form to amend.
+ * @param sName The field name.
+ * @param sValue The field value.
+ * @param sFieldId The field ID (required for updating).
+ */
+function addUpdateHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId)
+{
+ var oInputElement = null;
+ if (sFieldId)
+ {
+ oInputElement = document.getElementById(sFieldId);
+ }
+ if (oInputElement)
+ {
+ oInputElement.name = sName;
+ oInputElement.value = sValue;
+ }
+ else
+ {
+ oInputElement = addHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId);
+ }
+ return oInputElement;
+}
+
+/**
+ * Adds a width and a dpi input to the given form element if possible to
+ * determine the values.
+ *
+ * This is normally employed in an onlick hook, but then you must specify IDs or
+ * the browser may end up adding it several times.
+ *
+ * @param sFormId The ID of the form to amend.
+ * @param sWidthSrcId The ID of the element to calculate the width
+ * value from.
+ * @param sWidthName The name of the width value.
+ * @param sDpiName The name of the dpi value.
+ */
+function addDynamicGraphInputs(sFormId, sWidthSrcId, sWidthName, sDpiName)
+{
+ var cx = getUnscaledElementWidthById(sWidthSrcId);
+ var cDotsPerInch = getDeviceXDotsPerInch();
+
+ if (cx)
+ {
+ addUpdateHiddenInputFieldToFormById(sFormId, sWidthName, cx, sFormId + '-' + sWidthName + '-id');
+ }
+
+ if (cDotsPerInch)
+ {
+ addUpdateHiddenInputFieldToFormById(sFormId, sDpiName, cDotsPerInch, sFormId + '-' + sDpiName + '-id');
+ }
+
+}
+
+/**
+ * Adds the RedirecTo field with the current URL to the form.
+ *
+ * This is a 'onsubmit' action.
+ *
+ * @returns Returns success indicator (true/false).
+ * @param oForm The form being submitted.
+ */
+function addRedirectToInputFieldWithCurrentUrl(oForm)
+{
+ /* Constant used here is duplicated in WuiDispatcherBase.ksParamRedirectTo */
+ return addHiddenInputFieldToForm(oForm, 'RedirectTo', getCurrentBrowerUrlPartForRedirectTo(), null);
+}
+
+/**
+ * Adds the RedirecTo parameter to the href of the given anchor.
+ *
+ * This is a 'onclick' action.
+ *
+ * @returns Returns success indicator (true/false).
+ * @param oAnchor The anchor element being clicked on.
+ */
+function addRedirectToAnchorHref(oAnchor)
+{
+ var sRedirectToParam = g_ksParamRedirectTo + '=' + encodeURIComponent(getCurrentBrowerUrlPartForRedirectTo());
+ var sHref = oAnchor.href;
+ if (sHref.indexOf(sRedirectToParam) < 0)
+ {
+ var sHash;
+ var offHash = sHref.indexOf('#');
+ if (offHash >= 0)
+ sHash = sHref.substring(offHash);
+ else
+ {
+ sHash = '';
+ offHash = sHref.length;
+ }
+ sHref = sHref.substring(0, offHash)
+ if (sHref.indexOf('?') >= 0)
+ sHref += '&';
+ else
+ sHref += '?';
+ sHref += sRedirectToParam;
+ sHref += sHash;
+ oAnchor.href = sHref;
+ }
+ return true;
+}
+
+
+
+/**
+ * Clears one input element.
+ *
+ * @param oInput The input to clear.
+ */
+function resetInput(oInput)
+{
+ switch (oInput.type)
+ {
+ case 'checkbox':
+ case 'radio':
+ oInput.checked = false;
+ break;
+
+ case 'text':
+ oInput.value = 0;
+ break;
+ }
+}
+
+
+/**
+ * Clears a form.
+ *
+ * @param sIdForm The ID of the form
+ */
+function clearForm(sIdForm)
+{
+ var oForm = document.getElementById(sIdForm);
+ if (oForm)
+ {
+ var aoInputs = oForm.getElementsByTagName('INPUT');
+ var i;
+ for (i = 0; i < aoInputs.length; i++)
+ resetInput(aoInputs[i])
+
+ /* HTML5 allows inputs outside <form>, so scan the document. */
+ aoInputs = document.getElementsByTagName('INPUT');
+ for (i = 0; i < aoInputs.length; i++)
+ if (aoInputs.hasOwnProperty("form"))
+ if (aoInputs.form == sIdForm)
+ resetInput(aoInputs[i])
+ }
+
+ return true;
+}
+
+
+/**
+ * Used by the time navigation to update the hidden efficient date field when
+ * either of the date or time fields changes.
+ *
+ * @param oForm The form.
+ */
+function timeNavigationUpdateHiddenEffDate(oForm, sIdSuffix)
+{
+ var sDate = document.getElementById('EffDate' + sIdSuffix).value;
+ var sTime = document.getElementById('EffTime' + sIdSuffix).value;
+
+ var oField = document.getElementById('EffDateTime' + sIdSuffix);
+ oField.value = sDate + 'T' + sTime + '.00Z';
+}
+
+
+/** @name Collapsible / Expandable items
+ * @{
+ */
+
+
+/**
+ * Toggles the collapsible / expandable state of a parent DD and DT uncle.
+ *
+ * @returns true
+ * @param oAnchor The anchor object.
+ */
+function toggleCollapsibleDtDd(oAnchor)
+{
+ var oParent = oAnchor.parentElement;
+ var sClass = oParent.className;
+
+ /* Find the DD sibling tag */
+ var oDdElement = oParent.nextSibling;
+ while (oDdElement != null && oDdElement.tagName != 'DD')
+ oDdElement = oDdElement.nextSibling;
+
+ /* Determin the new class and arrow char. */
+ var sNewClass;
+ var sNewChar;
+ if ( sClass.substr(-11) == 'collapsible')
+ {
+ sNewClass = sClass.substr(0, sClass.length - 11) + 'expandable';
+ sNewChar = '\u25B6'; /* black right-pointing triangle */
+ }
+ else if (sClass.substr(-10) == 'expandable')
+ {
+ sNewClass = sClass.substr(0, sClass.length - 10) + 'collapsible';
+ sNewChar = '\u25BC'; /* black down-pointing triangle */
+ }
+ else
+ {
+ console.log('toggleCollapsibleParent: Invalid class: ' + sClass);
+ return true;
+ }
+
+ /* Update the parent (DT) class and anchor text. */
+ oParent.className = sNewClass;
+ oAnchor.firstChild.textContent = sNewChar + oAnchor.firstChild.textContent.substr(1);
+
+ /* Update the uncle (DD) class. */
+ if (oDdElement)
+ oDdElement.className = sNewClass;
+ return true;
+}
+
+/**
+ * Shows/hides a sub-category UL according to checkbox status.
+ *
+ * The checkbox is expected to be within a label element or something.
+ *
+ * @returns true
+ * @param oInput The input checkbox.
+ */
+function toggleCollapsibleCheckbox(oInput)
+{
+ var oParent = oInput.parentElement;
+
+ /* Find the UL sibling element. */
+ var oUlElement = oParent.nextSibling;
+ while (oUlElement != null && oUlElement.tagName != 'UL')
+ oUlElement = oUlElement.nextSibling;
+
+ /* Change the visibility. */
+ if (oInput.checked)
+ oUlElement.className = oUlElement.className.replace('expandable', 'collapsible');
+ else
+ {
+ oUlElement.className = oUlElement.className.replace('collapsible', 'expandable');
+
+ /* Make sure all sub-checkboxes are now unchecked. */
+ var aoSubInputs = oUlElement.getElementsByTagName('input');
+ var i;
+ for (i = 0; i < aoSubInputs.length; i++)
+ aoSubInputs[i].checked = false;
+ }
+ return true;
+}
+
+/**
+ * Toggles the sidebar size so filters can more easily manipulated.
+ */
+function toggleSidebarSize()
+{
+ var sLinkText;
+ if (document.body.className != 'tm-wide-side-menu')
+ {
+ document.body.className = 'tm-wide-side-menu';
+ sLinkText = '\u00ab\u00ab';
+ }
+ else
+ {
+ document.body.className = '';
+ sLinkText = '\u00bb\u00bb';
+ }
+
+ var aoToggleLink = document.getElementsByClassName('tm-sidebar-size-link');
+ var i;
+ for (i = 0; i < aoToggleLink.length; i++)
+ if ( aoToggleLink[i].textContent.indexOf('\u00bb') >= 0
+ || aoToggleLink[i].textContent.indexOf('\u00ab') >= 0)
+ aoToggleLink[i].textContent = sLinkText;
+}
+
+/** @} */
+
+
+/** @name Custom Tooltips
+ * @{
+ */
+
+/** Enables non-iframe tooltip code. */
+var g_fNewTooltips = true;
+
+/** Where we keep tooltip elements when not displayed. */
+var g_dTooltips = {};
+var g_oCurrentTooltip = null;
+var g_idTooltipShowTimer = null;
+var g_idTooltipHideTimer = null;
+var g_cTooltipSvnRevisions = 12;
+
+/**
+ * Cancel showing/replacing/repositing a tooltip.
+ */
+function tooltipResetShowTimer()
+{
+ if (g_idTooltipShowTimer)
+ {
+ clearTimeout(g_idTooltipShowTimer);
+ g_idTooltipShowTimer = null;
+ }
+}
+
+/**
+ * Cancel hiding of the current tooltip.
+ */
+function tooltipResetHideTimer()
+{
+ if (g_idTooltipHideTimer)
+ {
+ clearTimeout(g_idTooltipHideTimer);
+ g_idTooltipHideTimer = null;
+ }
+}
+
+/**
+ * Really hide the tooltip.
+ */
+function tooltipReallyHide()
+{
+ if (g_oCurrentTooltip)
+ {
+ //console.log('tooltipReallyHide: ' + g_oCurrentTooltip);
+ g_oCurrentTooltip.oElm.style.display = 'none';
+ g_oCurrentTooltip = null;
+ }
+}
+
+/**
+ * Schedule the tooltip for hiding.
+ */
+function tooltipHide()
+{
+ function tooltipDelayedHide()
+ {
+ tooltipResetHideTimer();
+ tooltipReallyHide();
+ }
+
+ /*
+ * Cancel any pending show and schedule hiding if necessary.
+ */
+ tooltipResetShowTimer();
+ if (g_oCurrentTooltip && !g_idTooltipHideTimer)
+ {
+ g_idTooltipHideTimer = setTimeout(tooltipDelayedHide, 700);
+ }
+
+ return true;
+}
+
+/**
+ * Function that is repositions the tooltip when it's shown.
+ *
+ * Used directly, via onload, and hackish timers to catch all browsers and
+ * whatnot.
+ *
+ * Will set several tooltip member variables related to position and space.
+ */
+function tooltipRepositionOnLoad()
+{
+ //console.log('tooltipRepositionOnLoad');
+ if (g_oCurrentTooltip)
+ {
+ var oRelToRect = g_oCurrentTooltip.oRelToRect;
+ var cxNeeded = g_oCurrentTooltip.oElm.offsetWidth + 8;
+ var cyNeeded = g_oCurrentTooltip.oElm.offsetHeight + 8;
+
+ var cyWindow = window.innerHeight;
+ var yScroll = window.pageYOffset || document.documentElement.scrollTop;
+ var yScrollBottom = yScroll + cyWindow;
+ var cxWindow = window.innerWidth;
+ var xScroll = window.pageXOffset || document.documentElement.scrollLeft;
+ var xScrollRight = xScroll + cxWindow;
+
+ var cyAbove = Math.max(oRelToRect.top, 0);
+ var cyBelow = Math.max(cyWindow - oRelToRect.bottom, 0);
+ var cxLeft = Math.max(oRelToRect.left, 0);
+ var cxRight = Math.max(cxWindow - oRelToRect.right, 0);
+
+ var xPos;
+ var yPos;
+
+ //console.log('tooltipRepositionOnLoad: rect: x,y=' + oRelToRect.x + ',' + oRelToRect.y
+ // + ' cx,cy=' + oRelToRect.width + ',' + oRelToRect.height + ' top=' + oRelToRect.top
+ // + ' bottom=' + oRelToRect.bottom + ' left=' + oRelToRect.left + ' right=' + oRelToRect.right);
+ //console.log('tooltipRepositionOnLoad: yScroll=' + yScroll + ' yScrollBottom=' + yScrollBottom);
+ //console.log('tooltipRepositionOnLoad: cyAbove=' + cyAbove + ' cyBelow=' + cyBelow + ' cyNeeded=' + cyNeeded);
+ //console.log('tooltipRepositionOnLoad: xScroll=' + xScroll + ' xScrollRight=' + xScrollRight);
+ //console.log('tooltipRepositionOnLoad: cxLeft=' + cxLeft + ' cxRight=' + cxRight + ' cxNeeded=' + cxNeeded);
+
+ /*
+ * Decide where to put the thing.
+ */
+ if (cyNeeded < cyBelow)
+ {
+ yPos = yScroll + oRelToRect.top;
+ g_oCurrentTooltip.cyMax = cyBelow;
+ //console.log('tooltipRepositionOnLoad: #1');
+ }
+ else if (cyBelow >= cyAbove)
+ {
+ yPos = yScrollBottom - cyNeeded;
+ g_oCurrentTooltip.cyMax = yScrollBottom - yPos;
+ //console.log('tooltipRepositionOnLoad: #2');
+ }
+ else
+ {
+ yPos = yScroll + oRelToRect.bottom - cyNeeded;
+ g_oCurrentTooltip.cyMax = yScrollBottom - yPos;
+ //console.log('tooltipRepositionOnLoad: #3');
+ }
+ if (yPos < yScroll)
+ {
+ yPos = yScroll;
+ g_oCurrentTooltip.cyMax = yScrollBottom - yPos;
+ //console.log('tooltipRepositionOnLoad: #4');
+ }
+ g_oCurrentTooltip.yPos = yPos;
+ g_oCurrentTooltip.yScroll = yScroll;
+ g_oCurrentTooltip.cyMaxUp = yPos - yScroll;
+ //console.log('tooltipRepositionOnLoad: yPos=' + yPos + ' yScroll=' + yScroll + ' cyMaxUp=' + g_oCurrentTooltip.cyMaxUp);
+
+ if (cxNeeded < cxRight)
+ {
+ xPos = xScroll + oRelToRect.right;
+ g_oCurrentTooltip.cxMax = cxRight;
+ //console.log('tooltipRepositionOnLoad: #5');
+ }
+ else
+ {
+ xPos = xScroll + oRelToRect.left - cxNeeded;
+ if (xPos < xScroll)
+ xPos = xScroll;
+ g_oCurrentTooltip.cxMax = cxNeeded;
+ //console.log('tooltipRepositionOnLoad: #6');
+ }
+ g_oCurrentTooltip.xPos = xPos;
+ g_oCurrentTooltip.xScroll = xScroll;
+ //console.log('tooltipRepositionOnLoad: xPos=' + xPos + ' xScroll=' + xScroll);
+
+ g_oCurrentTooltip.oElm.style.top = yPos + 'px';
+ g_oCurrentTooltip.oElm.style.left = xPos + 'px';
+ }
+ return true;
+}
+
+
+/**
+ * Really show the tooltip.
+ *
+ * @param oTooltip The tooltip object.
+ * @param oRelTo What to put the tooltip adjecent to.
+ */
+function tooltipReallyShow(oTooltip, oRelTo)
+{
+ var oRect;
+
+ tooltipResetShowTimer();
+ tooltipResetHideTimer();
+
+ if (g_oCurrentTooltip == oTooltip)
+ {
+ //console.log('moving tooltip');
+ }
+ else if (g_oCurrentTooltip)
+ {
+ //console.log('removing current tooltip and showing new');
+ tooltipReallyHide();
+ }
+ else
+ {
+ //console.log('showing tooltip');
+ }
+
+ //oTooltip.oElm.setAttribute('style', 'display: block; position: absolute;');
+ oTooltip.oElm.style.position = 'absolute';
+ oTooltip.oElm.style.display = 'block';
+ oRect = oRelTo.getBoundingClientRect();
+ oTooltip.oRelToRect = oRect;
+
+ g_oCurrentTooltip = oTooltip;
+
+ /*
+ * Do repositioning (again).
+ */
+ tooltipRepositionOnLoad();
+}
+
+/**
+ * Tooltip onmouseenter handler .
+ */
+function tooltipElementOnMouseEnter()
+{
+ /*console.log('tooltipElementOnMouseEnter: arguments.length='+arguments.length+' [0]='+arguments[0]);
+ console.log('ENT: currentTarget='+arguments[0].currentTarget+' id='+arguments[0].currentTarget.id+' class='+arguments[0].currentTarget.className); */
+ tooltipResetShowTimer();
+ tooltipResetHideTimer();
+ return true;
+}
+
+/**
+ * Tooltip onmouseout handler.
+ *
+ * @remarks We only use this and onmouseenter for one tooltip element (iframe
+ * for svn, because chrome is sending onmouseout events after
+ * onmouseneter for the next element, which would confuse this simple
+ * code.
+ */
+function tooltipElementOnMouseOut()
+{
+ var oEvt = arguments[0];
+ /*console.log('tooltipElementOnMouseOut: arguments.length='+arguments.length+' [0]='+oEvt);
+ console.log('OUT: currentTarget='+oEvt.currentTarget+' id='+oEvt.currentTarget.id+' class='+oEvt.currentTarget.className);*/
+
+ /* Ignore the event if leaving to a child element. */
+ var oElm = oEvt.toElement || oEvt.relatedTarget;
+ if (oElm != this && oElm)
+ {
+ for (;;)
+ {
+ oElm = oElm.parentNode;
+ if (!oElm || oElm == window)
+ break;
+ if (oElm == this)
+ {
+ console.log('OUT: was to child! - ignore');
+ return false;
+ }
+ }
+ }
+
+ tooltipHide();
+ return true;
+}
+
+/**
+ * iframe.onload hook that repositions and resizes the tooltip.
+ *
+ * This is a little hacky and we're calling it one or three times too many to
+ * work around various browser differences too.
+ */
+function svnHistoryTooltipOldOnLoad()
+{
+ //console.log('svnHistoryTooltipOldOnLoad');
+
+ /*
+ * Resize the tooltip to better fit the content.
+ */
+ tooltipRepositionOnLoad(); /* Sets cxMax and cyMax. */
+ if (g_oCurrentTooltip && g_oCurrentTooltip.oIFrame.contentWindow)
+ {
+ var oIFrameElement = g_oCurrentTooltip.oIFrame;
+ var cxSpace = Math.max(oIFrameElement.offsetLeft * 2, 0); /* simplified */
+ var cySpace = Math.max(oIFrameElement.offsetTop * 2, 0); /* simplified */
+ var cxNeeded = oIFrameElement.contentWindow.document.body.scrollWidth + cxSpace;
+ var cyNeeded = oIFrameElement.contentWindow.document.body.scrollHeight + cySpace;
+ var cx = Math.min(cxNeeded, g_oCurrentTooltip.cxMax);
+ var cy;
+
+ g_oCurrentTooltip.oElm.width = cx + 'px';
+ oIFrameElement.width = (cx - cxSpace) + 'px';
+ if (cx >= cxNeeded)
+ {
+ //console.log('svnHistoryTooltipOldOnLoad: overflowX -> hidden');
+ oIFrameElement.style.overflowX = 'hidden';
+ }
+ else
+ {
+ oIFrameElement.style.overflowX = 'scroll';
+ }
+
+ cy = Math.min(cyNeeded, g_oCurrentTooltip.cyMax);
+ if (cyNeeded > g_oCurrentTooltip.cyMax && g_oCurrentTooltip.cyMaxUp > 0)
+ {
+ var cyMove = Math.min(cyNeeded - g_oCurrentTooltip.cyMax, g_oCurrentTooltip.cyMaxUp);
+ g_oCurrentTooltip.cyMax += cyMove;
+ g_oCurrentTooltip.yPos -= cyMove;
+ g_oCurrentTooltip.oElm.style.top = g_oCurrentTooltip.yPos + 'px';
+ cy = Math.min(cyNeeded, g_oCurrentTooltip.cyMax);
+ }
+
+ g_oCurrentTooltip.oElm.height = cy + 'px';
+ oIFrameElement.height = (cy - cySpace) + 'px';
+ if (cy >= cyNeeded)
+ {
+ //console.log('svnHistoryTooltipOldOnLoad: overflowY -> hidden');
+ oIFrameElement.style.overflowY = 'hidden';
+ }
+ else
+ {
+ oIFrameElement.style.overflowY = 'scroll';
+ }
+
+ //console.log('cyNeeded='+cyNeeded+' cyMax='+g_oCurrentTooltip.cyMax+' cySpace='+cySpace+' cy='+cy);
+ //console.log('oIFrameElement.offsetTop='+oIFrameElement.offsetTop);
+ //console.log('svnHistoryTooltipOldOnLoad: cx='+cx+'cxMax='+g_oCurrentTooltip.cxMax+' cxNeeded='+cxNeeded+' cy='+cy+' cyMax='+g_oCurrentTooltip.cyMax);
+
+ tooltipRepositionOnLoad();
+ }
+ return true;
+}
+
+/**
+ * iframe.onload hook that repositions and resizes the tooltip.
+ *
+ * This is a little hacky and we're calling it one or three times too many to
+ * work around various browser differences too.
+ */
+function svnHistoryTooltipNewOnLoad()
+{
+ //console.log('svnHistoryTooltipNewOnLoad');
+
+ /*
+ * Resize the tooltip to better fit the content.
+ */
+ tooltipRepositionOnLoad(); /* Sets cxMax and cyMax. */
+ oTooltip = g_oCurrentTooltip;
+ if (oTooltip)
+ {
+ var oElmInner = oTooltip.oInnerElm;
+ var cxSpace = Math.max(oElmInner.offsetLeft * 2, 0); /* simplified */
+ var cySpace = Math.max(oElmInner.offsetTop * 2, 0); /* simplified */
+ var cxNeeded = oElmInner.scrollWidth + cxSpace;
+ var cyNeeded = oElmInner.scrollHeight + cySpace;
+ var cx = Math.min(cxNeeded, oTooltip.cxMax);
+
+ oTooltip.oElm.width = cx + 'px';
+ oElmInner.width = (cx - cxSpace) + 'px';
+ if (cx >= cxNeeded)
+ {
+ //console.log('svnHistoryTooltipNewOnLoad: overflowX -> hidden');
+ oElmInner.style.overflowX = 'hidden';
+ }
+ else
+ {
+ oElmInner.style.overflowX = 'scroll';
+ }
+
+ var cy = Math.min(cyNeeded, oTooltip.cyMax);
+ if (cyNeeded > oTooltip.cyMax && oTooltip.cyMaxUp > 0)
+ {
+ var cyMove = Math.min(cyNeeded - oTooltip.cyMax, oTooltip.cyMaxUp);
+ oTooltip.cyMax += cyMove;
+ oTooltip.yPos -= cyMove;
+ oTooltip.oElm.style.top = oTooltip.yPos + 'px';
+ cy = Math.min(cyNeeded, oTooltip.cyMax);
+ }
+
+ oTooltip.oElm.height = cy + 'px';
+ oElmInner.height = (cy - cySpace) + 'px';
+ if (cy >= cyNeeded)
+ {
+ //console.log('svnHistoryTooltipNewOnLoad: overflowY -> hidden');
+ oElmInner.style.overflowY = 'hidden';
+ }
+ else
+ {
+ oElmInner.style.overflowY = 'scroll';
+ }
+
+ //console.log('cyNeeded='+cyNeeded+' cyMax='+oTooltip.cyMax+' cySpace='+cySpace+' cy='+cy);
+ //console.log('oElmInner.offsetTop='+oElmInner.offsetTop);
+ //console.log('svnHistoryTooltipNewOnLoad: cx='+cx+'cxMax='+oTooltip.cxMax+' cxNeeded='+cxNeeded+' cy='+cy+' cyMax='+oTooltip.cyMax);
+
+ tooltipRepositionOnLoad();
+ }
+ return true;
+}
+
+
+function svnHistoryTooltipNewOnReadState(oTooltip, oRestReq, oParent)
+{
+ /*console.log('svnHistoryTooltipNewOnReadState: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState);*/
+ if (oRestReq.readyState != oRestReq.DONE)
+ {
+ oTooltip.oInnerElm.innerHTML = '<p>Loading ...(' + oRestReq.readyState + ')</p>';
+ return true;
+ }
+
+ /*
+ * Check the result and translate it to a javascript object (oResp).
+ */
+ var oResp = null;
+ var sHtml;
+ if (oRestReq.status != 200)
+ {
+ console.log('svnHistoryTooltipNewOnReadState: status=' + oRestReq.status);
+ sHtml = '<p>error: status=' + oRestReq.status + '</p>';
+ }
+ else
+ {
+ try
+ {
+ oResp = JSON.parse(oRestReq.responseText);
+ }
+ catch (oEx)
+ {
+ console.log('JSON.parse threw: ' + oEx.toString());
+ console.log(oRestReq.responseText);
+ sHtml = '<p>error: JSON.parse threw: ' + oEx.toString() + '</p>';
+ }
+ }
+
+ /*
+ * Generate the HTML.
+ *
+ * Note! Make sure the highlighting code in svnHistoryTooltipNewDelayedShow
+ * continues to work after modifying this code.
+ */
+ if (oResp)
+ {
+ sHtml = '<div class="tmvcstimeline tmvcstimelinetooltip">\n';
+
+ var aoCommits = oResp.aoCommits;
+ var cCommits = oResp.aoCommits.length;
+ var iCurDay = null;
+ var i;
+ for (i = 0; i < cCommits; i++)
+ {
+ var oCommit = aoCommits[i];
+ var tsCreated = parseIsoTimestamp(oCommit.tsCreated);
+ var iCommitDay = Math.floor((tsCreated.getTime() + tsCreated.getTimezoneOffset()) / (24 * 60 * 60 * 1000));
+ if (iCurDay === null || iCurDay != iCommitDay)
+ {
+ if (iCurDay !== null)
+ sHtml += ' </dl>\n';
+ iCurDay = iCommitDay;
+ sHtml += ' <h2>' + tsCreated.toISOString().split('T')[0] + ' ' + g_kasDaysOfTheWeek[tsCreated.getDay()] + '</h2>\n';
+ sHtml += ' <dl>\n';
+ }
+ Date
+
+ var sHighligh = '';
+ if (oCommit.iRevision == oTooltip.iRevision)
+ sHighligh += ' class="tmvcstimeline-highlight"';
+
+ sHtml += ' <dt id="r' + oCommit.iRevision + '"' + sHighligh + '>';
+ sHtml += '<a href="' + oResp.sTracChangesetUrlFmt.replace('%(iRevision)s', oCommit.iRevision.toString());
+ sHtml += '" target="_blank">';
+ sHtml += '<span class="tmvcstimeline-time">' + escapeElem(formatTimeHHMM(tsCreated, true)) + '</span>'
+ sHtml += ' Changeset <span class="tmvcstimeline-rev">[' + oCommit.iRevision + ']</span>';
+ sHtml += ' by <span class="tmvcstimeline-author">' + escapeElem(oCommit.sAuthor) + '</span>';
+ sHtml += '</a></dt>\n';
+ sHtml += ' <dd' + sHighligh + '>' + escapeElem(oCommit.sMessage) + '</dd>\n';
+ }
+
+ if (iCurDay !== null)
+ sHtml += ' </dl>\n';
+ sHtml += '</div>';
+ }
+
+ /*console.log('svnHistoryTooltipNewOnReadState: sHtml=' + sHtml);*/
+ oTooltip.oInnerElm.innerHTML = sHtml;
+
+ tooltipReallyShow(oTooltip, oParent);
+ svnHistoryTooltipNewOnLoad();
+}
+
+/**
+ * Calculates the last revision to get when showing a tooltip for @a iRevision.
+ *
+ * A tooltip covers several change log entries, both to limit the number of
+ * tooltips to load and to give context. The exact number is defined by
+ * g_cTooltipSvnRevisions.
+ *
+ * @returns Last revision in a tooltip.
+ * @param iRevision The revision number.
+ */
+function svnHistoryTooltipCalcLastRevision(iRevision)
+{
+ var iFirstRev = Math.floor(iRevision / g_cTooltipSvnRevisions) * g_cTooltipSvnRevisions;
+ return iFirstRev + g_cTooltipSvnRevisions - 1;
+}
+
+/**
+ * Calculates a unique ID for the tooltip element.
+ *
+ * This is also used as dictionary index.
+ *
+ * @returns tooltip ID value (string).
+ * @param sRepository The repository name.
+ * @param iRevision The revision number.
+ */
+function svnHistoryTooltipCalcId(sRepository, iRevision)
+{
+ return 'svnHistoryTooltip_' + sRepository + '_' + svnHistoryTooltipCalcLastRevision(iRevision);
+}
+
+/**
+ * The onmouseenter event handler for creating the tooltip.
+ *
+ * @param oEvt The event.
+ * @param sRepository The repository name.
+ * @param iRevision The revision number.
+ * @param sUrlPrefix URL prefix for non-testmanager use.
+ *
+ * @remarks onmouseout must be set to call tooltipHide.
+ */
+function svnHistoryTooltipShowEx(oEvt, sRepository, iRevision, sUrlPrefix)
+{
+ var sKey = svnHistoryTooltipCalcId(sRepository, iRevision);
+ var oParent = oEvt.currentTarget;
+ //console.log('svnHistoryTooltipShow ' + sRepository);
+
+ function svnHistoryTooltipOldDelayedShow()
+ {
+ var sSrc;
+
+ var oTooltip = g_dTooltips[sKey];
+ //console.log('svnHistoryTooltipOldDelayedShow ' + sRepository + ' ' + oTooltip);
+ if (!oTooltip)
+ {
+ /*
+ * Create a new tooltip element.
+ */
+ //console.log('creating ' + sKey);
+ oTooltip = {};
+ oTooltip.oElm = document.createElement('div');
+ oTooltip.oElm.setAttribute('id', sKey);
+ oTooltip.oElm.className = 'tmvcstooltip';
+ //oTooltip.oElm.setAttribute('style', 'display:none; position: absolute;');
+ oTooltip.oElm.style.display = 'none'; /* Note! Must stay hidden till loaded, or parent jumps with #rXXXX.*/
+ oTooltip.oElm.style.position = 'absolute';
+ oTooltip.oElm.style.zIndex = 6001;
+ oTooltip.xPos = 0;
+ oTooltip.yPos = 0;
+ oTooltip.cxMax = 0;
+ oTooltip.cyMax = 0;
+ oTooltip.cyMaxUp = 0;
+ oTooltip.xScroll = 0;
+ oTooltip.yScroll = 0;
+ oTooltip.iRevision = iRevision; /**< For :target/highlighting */
+
+ var oIFrameElement = document.createElement('iframe');
+ oIFrameElement.setAttribute('id', sKey + '_iframe');
+ oIFrameElement.style.position = 'relative';
+ oIFrameElement.onmouseenter = tooltipElementOnMouseEnter;
+ //oIFrameElement.onmouseout = tooltipElementOnMouseOut;
+ oTooltip.oElm.appendChild(oIFrameElement);
+ oTooltip.oIFrame = oIFrameElement;
+ g_dTooltips[sKey] = oTooltip;
+
+ document.body.appendChild(oTooltip.oElm);
+
+ oIFrameElement.onload = function() { /* A slight delay here to give time for #rXXXX scrolling before we show it. */
+ setTimeout(function(){
+ /*console.log('iframe/onload');*/
+ tooltipReallyShow(oTooltip, oParent);
+ svnHistoryTooltipOldOnLoad();
+ }, isBrowserInternetExplorer() ? 256 : 128);
+ };
+
+ var sUrl = sUrlPrefix + 'index.py?Action=VcsHistoryTooltip&repo=' + sRepository
+ + '&rev=' + svnHistoryTooltipCalcLastRevision(iRevision)
+ + '&cEntries=' + g_cTooltipSvnRevisions
+ + '#r' + iRevision;
+ oIFrameElement.src = sUrl;
+ }
+ else
+ {
+ /*
+ * Show the existing one, possibly with different :target/highlighting.
+ */
+ if (oTooltip.iRevision != iRevision)
+ {
+ //console.log('Changing revision ' + oTooltip.iRevision + ' -> ' + iRevision);
+ oTooltip.oIFrame.contentWindow.location.hash = '#r' + iRevision;
+ if (!isBrowserFirefox()) /* Chrome updates stuff like expected; Firefox OTOH doesn't change anything. */
+ {
+ setTimeout(function() { /* Slight delay to make sure it scrolls before it's shown. */
+ tooltipReallyShow(oTooltip, oParent);
+ svnHistoryTooltipOldOnLoad();
+ }, isBrowserInternetExplorer() ? 256 : 64);
+ }
+ else
+ oTooltip.oIFrame.contentWindow.location.reload();
+ }
+ else
+ {
+ tooltipReallyShow(oTooltip, oParent);
+ svnHistoryTooltipOldOnLoad();
+ }
+ }
+ }
+
+ function svnHistoryTooltipNewDelayedShow()
+ {
+ var sSrc;
+
+ var oTooltip = g_dTooltips[sKey];
+ /*console.log('svnHistoryTooltipNewDelayedShow: ' + sRepository + ' ' + oTooltip);*/
+ if (!oTooltip)
+ {
+ /*
+ * Create a new tooltip element.
+ */
+ /*console.log('creating ' + sKey);*/
+
+ var oElm = document.createElement('div');
+ oElm.setAttribute('id', sKey);
+ oElm.className = 'tmvcstooltipnew';
+ //oElm.setAttribute('style', 'display:none; position: absolute;');
+ oElm.style.display = 'none'; /* Note! Must stay hidden till loaded, or parent jumps with #rXXXX.*/
+ oElm.style.position = 'absolute';
+ oElm.style.zIndex = 6001;
+ oElm.onmouseenter = tooltipElementOnMouseEnter;
+ oElm.onmouseout = tooltipElementOnMouseOut;
+
+ var oInnerElm = document.createElement('div');
+ oInnerElm.className = 'tooltip-inner';
+ oElm.appendChild(oInnerElm);
+
+ oTooltip = {};
+ oTooltip.oElm = oElm;
+ oTooltip.oInnerElm = oInnerElm;
+ oTooltip.xPos = 0;
+ oTooltip.yPos = 0;
+ oTooltip.cxMax = 0;
+ oTooltip.cyMax = 0;
+ oTooltip.cyMaxUp = 0;
+ oTooltip.xScroll = 0;
+ oTooltip.yScroll = 0;
+ oTooltip.iRevision = iRevision; /**< For :target/highlighting */
+
+ oRestReq = new XMLHttpRequest();
+ oRestReq.onreadystatechange = function() { svnHistoryTooltipNewOnReadState(oTooltip, this, oParent); }
+ oRestReq.open('GET', sUrlPrefix + 'rest.py?sPath=vcs/changelog/' + sRepository
+ + '/' + svnHistoryTooltipCalcLastRevision(iRevision) + '/' + g_cTooltipSvnRevisions);
+ oRestReq.setRequestHeader('Content-type', 'application/json');
+
+ document.body.appendChild(oTooltip.oElm);
+ g_dTooltips[sKey] = oTooltip;
+
+ oRestReq.send('');
+ }
+ else
+ {
+ /*
+ * Show the existing one, possibly with different highlighting.
+ * Note! Update this code when changing svnHistoryTooltipNewOnReadState.
+ */
+ if (oTooltip.iRevision != iRevision)
+ {
+ //console.log('Changing revision ' + oTooltip.iRevision + ' -> ' + iRevision);
+ var oElmTimelineDiv = oTooltip.oInnerElm.firstElementChild;
+ var i;
+ for (i = 0; i < oElmTimelineDiv.children.length; i++)
+ {
+ var oElm = oElmTimelineDiv.children[i];
+ //console.log('oElm='+oElm+' id='+oElm.id+' nodeName='+oElm.nodeName);
+ if (oElm.nodeName == 'DL')
+ {
+ var iCurRev = iRevision - 64;
+ var j;
+ for (j = 0; i < oElm.children.length; i++)
+ {
+ var oDlSubElm = oElm.children[i];
+ //console.log(' oDlSubElm='+oDlSubElm+' id='+oDlSubElm.id+' nodeName='+oDlSubElm.nodeName+' className='+oDlSubElm.className);
+ if (oDlSubElm.id.length > 2)
+ iCurRev = parseInt(oDlSubElm.id.substring(1), 10);
+ if (iCurRev == iRevision)
+ oDlSubElm.className = 'tmvcstimeline-highlight';
+ else
+ oDlSubElm.className = '';
+ }
+ }
+ }
+ oTooltip.iRevision = iRevision;
+ }
+
+ tooltipReallyShow(oTooltip, oParent);
+ svnHistoryTooltipNewOnLoad();
+ }
+ }
+
+
+ /*
+ * Delay the change (in case the mouse moves on).
+ */
+ tooltipResetShowTimer();
+ if (g_fNewTooltips)
+ g_idTooltipShowTimer = setTimeout(svnHistoryTooltipNewDelayedShow, 512);
+ else
+ g_idTooltipShowTimer = setTimeout(svnHistoryTooltipOldDelayedShow, 512);
+}
+
+/**
+ * The onmouseenter event handler for creating the tooltip.
+ *
+ * @param oEvt The event.
+ * @param sRepository The repository name.
+ * @param iRevision The revision number.
+ *
+ * @remarks onmouseout must be set to call tooltipHide.
+ */
+function svnHistoryTooltipShow(oEvt, sRepository, iRevision)
+{
+ return svnHistoryTooltipShowEx(oEvt, sRepository, iRevision, '');
+}
+
+/** @} */
+
+
+/** @name Debugging and Introspection
+ * @{
+ */
+
+/**
+ * Python-like dir() implementation.
+ *
+ * @returns Array of names associated with oObj.
+ * @param oObj The object under inspection. If not specified we'll
+ * look at the window object.
+ */
+function pythonlikeDir(oObj, fDeep)
+{
+ var aRet = [];
+ var dTmp = {};
+
+ if (!oObj)
+ {
+ oObj = window;
+ }
+
+ for (var oCur = oObj; oCur; oCur = Object.getPrototypeOf(oCur))
+ {
+ var aThis = Object.getOwnPropertyNames(oCur);
+ for (var i = 0; i < aThis.length; i++)
+ {
+ if (!(aThis[i] in dTmp))
+ {
+ dTmp[aThis[i]] = 1;
+ aRet.push(aThis[i]);
+ }
+ }
+ }
+
+ return aRet;
+}
+
+
+/**
+ * Python-like dir() implementation, shallow version.
+ *
+ * @returns Array of names associated with oObj.
+ * @param oObj The object under inspection. If not specified we'll
+ * look at the window object.
+ */
+function pythonlikeShallowDir(oObj, fDeep)
+{
+ var aRet = [];
+ var dTmp = {};
+
+ if (oObj)
+ {
+ for (var i in oObj)
+ {
+ aRet.push(i);
+ }
+ }
+
+ return aRet;
+}
+
+
+
+function dbgGetObjType(oObj)
+{
+ var sType = typeof oObj;
+ if (sType == "object" && oObj !== null)
+ {
+ if (oObj.constructor && oObj.constructor.name)
+ {
+ sType = oObj.constructor.name;
+ }
+ else
+ {
+ var fnToString = Object.prototype.toString;
+ var sTmp = fnToString.call(oObj);
+ if (sTmp.indexOf('[object ') === 0)
+ {
+ sType = sTmp.substring(8, sTmp.length);
+ }
+ }
+ }
+ return sType;
+}
+
+
+/**
+ * Dumps the given object to the console.
+ *
+ * @param oObj The object under inspection.
+ * @param sPrefix What to prefix the log output with.
+ */
+function dbgDumpObj(oObj, sName, sPrefix)
+{
+ var aMembers;
+ var sType;
+
+ /*
+ * Defaults
+ */
+ if (!oObj)
+ {
+ oObj = window;
+ }
+
+ if (!sPrefix)
+ {
+ if (sName)
+ {
+ sPrefix = sName + ':';
+ }
+ else
+ {
+ sPrefix = 'dbgDumpObj:';
+ }
+ }
+
+ if (!sName)
+ {
+ sName = '';
+ }
+
+ /*
+ * The object itself.
+ */
+ sPrefix = sPrefix + ' ';
+ console.log(sPrefix + sName + ' ' + dbgGetObjType(oObj));
+
+ /*
+ * The members.
+ */
+ sPrefix = sPrefix + ' ';
+ aMembers = pythonlikeDir(oObj);
+ for (i = 0; i < aMembers.length; i++)
+ {
+ console.log(sPrefix + aMembers[i]);
+ }
+
+ return true;
+}
+
+function dbgDumpObjWorker(sType, sName, oObj, sPrefix)
+{
+ var sRet;
+ switch (sType)
+ {
+ case 'function':
+ {
+ sRet = sPrefix + 'function ' + sName + '()' + '\n';
+ break;
+ }
+
+ case 'object':
+ {
+ sRet = sPrefix + 'var ' + sName + '(' + dbgGetObjType(oObj) + ') =';
+ if (oObj !== null)
+ {
+ sRet += '\n';
+ }
+ else
+ {
+ sRet += ' null\n';
+ }
+ break;
+ }
+
+ case 'string':
+ {
+ sRet = sPrefix + 'var ' + sName + '(string, ' + oObj.length + ')';
+ if (oObj.length < 80)
+ {
+ sRet += ' = "' + oObj + '"\n';
+ }
+ else
+ {
+ sRet += '\n';
+ }
+ break;
+ }
+
+ case 'Oops!':
+ sRet = sPrefix + sName + '(??)\n';
+ break;
+
+ default:
+ sRet = sPrefix + 'var ' + sName + '(' + sType + ')\n';
+ break;
+ }
+ return sRet;
+}
+
+
+function dbgObjInArray(aoObjs, oObj)
+{
+ var i = aoObjs.length;
+ while (i > 0)
+ {
+ i--;
+ if (aoObjs[i] === oObj)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+function dbgDumpObjTreeWorker(oObj, sPrefix, aParentObjs, cMaxDepth)
+{
+ var sRet = '';
+ var aMembers = pythonlikeShallowDir(oObj);
+ var i;
+
+ for (i = 0; i < aMembers.length; i++)
+ {
+ //var sName = i;
+ var sName = aMembers[i];
+ var oMember;
+ var sType;
+ var oEx;
+
+ try
+ {
+ oMember = oObj[sName];
+ sType = typeof oObj[sName];
+ }
+ catch (oEx)
+ {
+ oMember = null;
+ sType = 'Oops!';
+ }
+
+ //sRet += '[' + i + '/' + aMembers.length + ']';
+ sRet += dbgDumpObjWorker(sType, sName, oMember, sPrefix);
+
+ if ( sType == 'object'
+ && oObj !== null)
+ {
+
+ if (dbgObjInArray(aParentObjs, oMember))
+ {
+ sRet += sPrefix + '! parent recursion\n';
+ }
+ else if ( sName == 'previousSibling'
+ || sName == 'previousElement'
+ || sName == 'lastChild'
+ || sName == 'firstElementChild'
+ || sName == 'lastElementChild'
+ || sName == 'nextElementSibling'
+ || sName == 'prevElementSibling'
+ || sName == 'parentElement'
+ || sName == 'ownerDocument')
+ {
+ sRet += sPrefix + '! potentially dangerous element name\n';
+ }
+ else if (aParentObjs.length >= cMaxDepth)
+ {
+ sRet = sRet.substring(0, sRet.length - 1);
+ sRet += ' <too deep>!\n';
+ }
+ else
+ {
+
+ aParentObjs.push(oMember);
+ if (i + 1 < aMembers.length)
+ {
+ sRet += dbgDumpObjTreeWorker(oMember, sPrefix + '| ', aParentObjs, cMaxDepth);
+ }
+ else
+ {
+ sRet += dbgDumpObjTreeWorker(oMember, sPrefix.substring(0, sPrefix.length - 2) + ' | ', aParentObjs, cMaxDepth);
+ }
+ aParentObjs.pop();
+ }
+ }
+ }
+ return sRet;
+}
+
+/**
+ * Dumps the given object and all it's subobjects to the console.
+ *
+ * @returns String dump of the object.
+ * @param oObj The object under inspection.
+ * @param sName The object name (optional).
+ * @param sPrefix What to prefix the log output with (optional).
+ * @param cMaxDepth The max depth, optional.
+ */
+function dbgDumpObjTree(oObj, sName, sPrefix, cMaxDepth)
+{
+ var sType;
+ var sRet;
+ var oEx;
+
+ /*
+ * Defaults
+ */
+ if (!sPrefix)
+ {
+ sPrefix = '';
+ }
+
+ if (!sName)
+ {
+ sName = '??';
+ }
+
+ if (!cMaxDepth)
+ {
+ cMaxDepth = 2;
+ }
+
+ /*
+ * The object itself.
+ */
+ try
+ {
+ sType = typeof oObj;
+ }
+ catch (oEx)
+ {
+ sType = 'Oops!';
+ }
+ sRet = dbgDumpObjWorker(sType, sName, oObj, sPrefix);
+ if (sType == 'object' && oObj !== null)
+ {
+ var aParentObjs = Array();
+ aParentObjs.push(oObj);
+ sRet += dbgDumpObjTreeWorker(oObj, sPrefix + '| ', aParentObjs, cMaxDepth);
+ }
+
+ return sRet;
+}
+
+function dbgLogString(sLongString)
+{
+ var aStrings = sLongString.split("\n");
+ var i;
+ for (i = 0; i < aStrings.length; i++)
+ {
+ console.log(aStrings[i]);
+ }
+ console.log('dbgLogString - end - ' + aStrings.length + '/' + sLongString.length);
+ return true;
+}
+
+function dbgLogObjTree(oObj, sName, sPrefix, cMaxDepth)
+{
+ return dbgLogString(dbgDumpObjTree(oObj, sName, sPrefix, cMaxDepth));
+}
+
+/** @} */
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js b/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js
new file mode 100644
index 00000000..90e1163d
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js
@@ -0,0 +1,126 @@
+/* $Id: graphwiz.js $ */
+/** @file
+ * JavaScript functions for the Graph Wizard.
+ */
+
+/*
+ * Copyright (C) 2012-2023 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+ * in the VirtualBox distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+ */
+
+
+/*******************************************************************************
+* Global Variables *
+*******************************************************************************/
+/** The previous width of the div element that we measure. */
+var g_cxPreviousWidth = 0;
+
+
+/**
+ * onload function that sets g_cxPreviousWidth to the width of @a sWidthSrcId.
+ *
+ * @returns true.
+ * @param sWidthSrcId The ID of the element which width we should measure.
+ */
+function graphwizOnLoadRememberWidth(sWidthSrcId)
+{
+ var cx = getUnscaledElementWidthById(sWidthSrcId);
+ if (cx)
+ {
+ g_cxPreviousWidth = cx;
+ }
+ return true;
+}
+
+
+/**
+ * onresize callback function that scales the given graph width input field
+ * value according to the resized element.
+ *
+ * @returns true.
+ * @param sWidthSrcId The ID of the element which width we should measure
+ * the resize effect on.
+ * @param sWidthInputId The ID of the input field which values should be
+ * scaled.
+ *
+ * @remarks Since we're likely to get several resize calls as part of one user
+ * resize operation, we're likely to suffer from some rounding
+ * artifacts. So, should the user abort or undo the resizing, the
+ * width value is unlikely to be restored to the exact value it had
+ * prior to the resizing.
+ */
+function graphwizOnResizeRecalcWidth(sWidthSrcId, sWidthInputId)
+{
+ var cx = getUnscaledElementWidthById(sWidthSrcId);
+ if (cx)
+ {
+ var oElement = document.getElementById(sWidthInputId);
+ if (oElement && g_cxPreviousWidth)
+ {
+ var cxOld = oElement.value;
+ if (isInteger(cxOld))
+ {
+ var fpRatio = cxOld / g_cxPreviousWidth;
+ oElement.value = Math.round(cx * fpRatio);
+ }
+ }
+ g_cxPreviousWidth = cx;
+ }
+
+ return true;
+}
+
+/**
+ * Fills thegraph size (cx, cy) and dpi fields with default values.
+ *
+ * @returns false (for onclick).
+ * @param sWidthSrcId The ID of the element which width we should measure.
+ * @param sWidthInputId The ID of the graph width field (cx).
+ * @param sHeightInputId The ID of the graph height field (cy).
+ * @param sDpiInputId The ID of the graph DPI field.
+ */
+function graphwizSetDefaultSizeValues(sWidthSrcId, sWidthInputId, sHeightInputId, sDpiInputId)
+{
+ var cx = getUnscaledElementWidthById(sWidthSrcId);
+ var cDotsPerInch = getDeviceXDotsPerInch();
+
+ if (cx)
+ {
+ setInputFieldValue(sWidthInputId, cx);
+ setInputFieldValue(sHeightInputId, Math.round(cx * 5 / 16)); /* See wuimain.py. */
+ }
+
+ if (cDotsPerInch)
+ {
+ setInputFieldValue(sDpiInputId, cDotsPerInch);
+ }
+
+ return false;
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js b/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js
new file mode 100644
index 00000000..f7b7de7c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js
@@ -0,0 +1,237 @@
+/* $Id: vcsrevisions.js $ */
+/** @file
+ * Common JavaScript functions
+ */
+
+/*
+ * Copyright (C) 2012-2023 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+ * in the VirtualBox distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+ */
+
+
+/**
+ * @internal.
+ */
+function vcsRevisionFormatDate(tsDate)
+{
+ /*return tsDate.toLocaleDateString();*/
+ return tsDate.toISOString().split('T')[0];
+}
+
+/**
+ * @internal.
+ */
+function vcsRevisionFormatTime(tsDate)
+{
+ return formatTimeHHMM(tsDate, true /*fNbsp*/);
+}
+
+/**
+ * Called 'onclick' for the link/button used to show the detailed VCS
+ * revisions.
+ * @internal.
+ */
+function vcsRevisionShowDetails(oElmSource)
+{
+ document.getElementById('vcsrevisions-detailed').style.display = 'block';
+ document.getElementById('vcsrevisions-brief').style.display = 'none';
+ oElmSource.style.display = 'none';
+ return false;
+}
+
+/**
+ * Called when we've got the revision data.
+ * @internal
+ */
+function vcsRevisionsRender(sTestMgr, oElmDst, sBugTracker, oRestReq, sUrl)
+{
+ console.log('vcsRevisionsRender: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState + ' url=' + sUrl);
+ if (oRestReq.readyState != oRestReq.DONE)
+ {
+ oElmDst.innerHTML = '<p>' + oRestReq.readyState + '</p>';
+ return true;
+ }
+
+
+ /*
+ * Check the result and translate it to a javascript object (oResp).
+ */
+ var oResp = null;
+ var sHtml;
+ if (oRestReq.status != 200)
+ {
+ /** @todo figure why this doesn't work (sPath to something random). */
+ var sMsg = oRestReq.getResponseHeader('tm-error-message');
+ console.log('vcsRevisionsRender: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState + ' url=' + sUrl + ' msg=' + sMsg);
+ sHtml = '<p>error: status=' + oRestReq.status + 'readyState=' + oRestReq.readyState + ' url=' + sUrl;
+ if (sMsg)
+ sHtml += ' msg=' + sMsg;
+ sHtml += '</p>';
+ }
+ else
+ {
+ try
+ {
+ oResp = JSON.parse(oRestReq.responseText);
+ }
+ catch (oEx)
+ {
+ console.log('JSON.parse threw: ' + oEx.toString());
+ console.log(oRestReq.responseText);
+ sHtml = '<p>error: JSON.parse threw: ' + oEx.toString() + '</p>';
+ }
+ }
+
+ /*
+ * Do the rendering.
+ */
+ if (oResp)
+ {
+ if (oResp.cCommits == 0)
+ {
+ sHtml = '<p>None.</p>';
+ }
+ else
+ {
+ var aoCommits = oResp.aoCommits;
+ var cCommits = oResp.aoCommits.length;
+ var i;
+
+ sHtml = '';
+ /*sHtml = '<a href="#" onclick="return vcsRevisionShowDetails(this);" class="vcsrevisions-show-details">Show full VCS details...</a>\n';*/
+ /*sHtml = '<button onclick="vcsRevisionShowDetails(this);" class="vcsrevisions-show-details">Show full VCS details...</button>\n';*/
+
+ /* Brief view (the default): */
+ sHtml += '<p id="vcsrevisions-brief">';
+ for (i = 0; i < cCommits; i++)
+ {
+ var oCommit = aoCommits[i];
+ var sUrl = oResp.sTracChangesetUrlFmt.replace('%(sRepository)s', oCommit.sRepository).replace('%(iRevision)s', oCommit.iRevision.toString());
+ var sTitle = oCommit.sAuthor + ': ' + oCommit.sMessage;
+ sHtml += ' <a href="' + escapeElem(sUrl) + '" title="' + escapeElem(sTitle) + '">r' + oCommit.iRevision + '</a> \n';
+ }
+ sHtml += '</p>';
+ sHtml += '<a href="#" onclick="return vcsRevisionShowDetails(this);" class="vcsrevisions-show-details-bottom">Show full VCS details...</a>\n';
+
+ /* Details view: */
+ sHtml += '<div id="vcsrevisions-detailed" style="display:none;">\n';
+ var iCurDay = null;
+ if (0)
+ {
+ /* Changelog variant: */
+ for (i = 0; i < cCommits; i++)
+ {
+ var oCommit = aoCommits[i];
+ var tsCreated = parseIsoTimestamp(oCommit.tsCreated);
+ var sUrl = oResp.sTracChangesetUrlFmt.replace('%(sRepository)s', oCommit.sRepository).replace('%(iRevision)s', oCommit.iRevision.toString());
+ var iCommitDay = Math.floor((tsCreated.getTime() + tsCreated.getTimezoneOffset()) / (24 * 60 * 60 * 1000));
+ if (iCurDay === null || iCurDay != iCommitDay)
+ {
+ if (iCurDay !== null)
+ sHtml += ' </dl>\n';
+ iCurDay = iCommitDay;
+ sHtml += ' <h3>' + vcsRevisionFormatDate(tsCreated) + ' ' + g_kasDaysOfTheWeek[tsCreated.getDay()] + '</h3>\n';
+ sHtml += ' <dl>\n';
+ }
+
+ sHtml += ' <dt id="r' + oCommit.iRevision + '">';
+ sHtml += '<a href="' + oResp.sTracChangesetUrlFmt.replace('%(iRevision)s', oCommit.iRevision.toString()) + '">';
+ /*sHtml += '<span class="vcsrevisions-time">' + escapeElem(vcsRevisionFormatTime(tsCreated)) + '</span>'
+ sHtml += ' Changeset <span class="vcsrevisions-rev">r' + oCommit.iRevision + '</span>';
+ sHtml += ' by <span class="vcsrevisions-author">' + escapeElem(oCommit.sAuthor) + '</span>'; */
+ sHtml += '<span class="vcsrevisions-time">' + escapeElem(vcsRevisionFormatTime(tsCreated)) + '</span>';
+ sHtml += ' - <span class="vcsrevisions-rev">r' + oCommit.iRevision + '</span>';
+ sHtml += ' - <span class="vcsrevisions-author">' + escapeElem(oCommit.sAuthor) + '</span>';
+ sHtml += '</a></dt>\n';
+ sHtml += ' <dd>' + escapeElem(oCommit.sMessage) + '</dd>\n';
+ }
+
+ if (iCurDay !== null)
+ sHtml += ' </dl>\n';
+ }
+ else
+ { /* TABLE variant: */
+ sHtml += '<table class="vcsrevisions-table">';
+ var iAlt = 0;
+ for (i = 0; i < cCommits; i++)
+ {
+ var oCommit = aoCommits[i];
+ var tsCreated = parseIsoTimestamp(oCommit.tsCreated);
+ var sUrl = oResp.sTracChangesetUrlFmt.replace('%(sRepository)s', oCommit.sRepository).replace('%(iRevision)s', oCommit.iRevision.toString());
+ var iCommitDay = Math.floor((tsCreated.getTime() + tsCreated.getTimezoneOffset()) / (24 * 60 * 60 * 1000));
+ if (iCurDay === null || iCurDay != iCommitDay)
+ {
+ iCurDay = iCommitDay;
+ sHtml += '<tr id="r' + oCommit.iRevision + '"><td colspan="4" class="vcsrevisions-tab-date">';
+ sHtml += vcsRevisionFormatDate(tsCreated) + ' ' + g_kasDaysOfTheWeek[tsCreated.getDay()];
+ sHtml += '</td></tr>\n';
+ sHtml += '<tr>';
+ iAlt = 0;
+ }
+ else
+ sHtml += '<tr id="r' + oCommit.iRevision + '">';
+ var sAltCls = '';
+ var sAltClsStmt = '';
+ iAlt += 1;
+ if (iAlt & 1)
+ {
+ sAltCls = ' alt';
+ sAltClsStmt = ' class="alt"';
+ }
+ sHtml += '<td class="vcsrevisions-tab-time'+sAltCls+'"><a href="' + sUrl + '">'
+ + escapeElem(vcsRevisionFormatTime(tsCreated)) + '</a></td>';
+ sHtml += '<td'+sAltClsStmt+'><a href="' + sUrl + '" class="vcsrevisions-rev' + sAltCls + '">r'
+ + oCommit.iRevision + '</a></td>';
+ sHtml += '<td'+sAltClsStmt+'><a href="' + sUrl + '" class="vcsrevisions-author' + sAltCls + '">'
+ + escapeElem(oCommit.sAuthor) + '<a></td>';
+ sHtml += '<td'+sAltClsStmt+'>' + escapeElem(oCommit.sMessage) + '</td></tr>\n';
+ }
+ sHtml += '</table>\n';
+ }
+ sHtml += '</div>\n';
+ }
+ }
+
+ oElmDst.innerHTML = sHtml;
+}
+
+/** Called by the xtracker bugdetails page. */
+function VcsRevisionsLoad(sTestMgr, oElmDst, sBugTracker, lBugNo)
+{
+ oElmDst.innerHTML = '<p>Loading VCS revisions...</p>';
+
+ var sUrl = sTestMgr + 'rest.py?sPath=vcs/bugreferences/' + sBugTracker + '/' + lBugNo;
+ var oRestReq = new XMLHttpRequest();
+ oRestReq.onreadystatechange = function() { vcsRevisionsRender(sTestMgr, oElmDst, sBugTracker, this, sUrl); }
+ oRestReq.open('GET', sUrl);
+ oRestReq.withCredentials = true;
+ /*oRestReq.setRequestHeader('Content-type', 'application/json'); - Causes CORS trouble. */
+ oRestReq.send();
+}
+
diff --git a/src/VBox/ValidationKit/testmanager/misc/Makefile.kmk b/src/VBox/ValidationKit/testmanager/misc/Makefile.kmk
new file mode 100644
index 00000000..74d882cc
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/misc/Makefile.kmk
@@ -0,0 +1,46 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+SUB_DEPTH = ../../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+
+VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py)
+
+$(evalcall def_vbox_validationkit_process_python_sources)
+$(evalcall def_vbox_validationkit_process_js_sources)
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/testmanager/misc/htpasswd-logout b/src/VBox/ValidationKit/testmanager/misc/htpasswd-logout
new file mode 100644
index 00000000..8a36998b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/misc/htpasswd-logout
@@ -0,0 +1 @@
+logout:$apr1$OqiMc/Uv$XylAjnIPla7gb57UMW0TK.
diff --git a/src/VBox/ValidationKit/testmanager/misc/htpasswd-sample b/src/VBox/ValidationKit/testmanager/misc/htpasswd-sample
new file mode 100644
index 00000000..6b6c1b33
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/misc/htpasswd-sample
@@ -0,0 +1,2 @@
+admin:ZXHvyrLs.vCmw
+test:ClO2uu6/D7jDg
diff --git a/src/VBox/ValidationKit/testmanager/readme.txt b/src/VBox/ValidationKit/testmanager/readme.txt
new file mode 100644
index 00000000..7211c7b6
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/readme.txt
@@ -0,0 +1,125 @@
+$Id: readme.txt $
+
+Directory descriptions:
+ ./ The Test Manager.
+ ./batch/ Batch scripts to be run via cron.
+ ./cgi/ CGI scripts (we'll use standard CGI at first).
+ ./core/ The core Test Manager logic (model).
+ ./htdocs/ Files to be served directly by the web server.
+ ./htdocs/css/ Style sheets.
+ ./htdocs/images/ Graphics.
+ ./webui/ The Web User Interface (WUI) bits. (Not sure if we will
+ do model-view-controller stuff, though. Time will show.)
+
+I. Running a Test Manager instance with Docker:
+
+ - This way should be preferred to get a local Test Manager instance running
+ and is NOT meant for production use!
+
+ - Install docker-ce and docker-compose on your Linux host (not tested on
+ Windows yet). Your user must be able to run the Docker CLI (see Docker documentation).
+
+ - Type "kmk" to get the containers built, "kmk start|stop" to start/stop them
+ respectively. To start over, use "kmk clean". For having a peek into the container
+ logs, use "kmk logs".
+
+ To administrate / develop the database, an Adminer instance is running at
+ http://localhost:8080
+
+ To access the actual Test Manager instance, go to http://localhost:8080/testmanager/
+
+ - There are two ways of doing development with this setup:
+
+ a. The Test Manager source is stored inside a separate data volume called
+ "docker_vbox-testmgr-web". The source will be checked out automatically on
+ container initialization. Development then can take part within that data
+ container. The initialization script will automatically pull the sources
+ from the public OSE tree, so make sure this is what you want!
+
+ b. Edit the (hidden) .env file in this directory and change VBOX_TESTMGR_DATA
+ to point to your checked out VBox root, e.g. VBOX_TESTMGR_DATA=/path/to/VBox/trunk
+
+
+II. Steps for manually setting up a local Test Manager instance for development:
+
+ - Install apache, postgresql, python, psycopg2 (python) and pylint.
+
+ - Create the database by executing 'kmk load-testmanager-db' in
+ the './db/' subdirectory. The default psql parameters there
+ requies pg_hba.conf to specify 'trust' instead of 'peer' as the
+ authentication method for local connections.
+
+ - Use ./db/partial-db-dump.py on the production system to extract a
+ partial database dump (last 14 days).
+
+ - Use ./db/partial-db-dump.py with the --load-dump-into-database
+ parameter on the development box to load the dump.
+
+ - Configure apache using the ./apache-template-2.4.conf (see top of
+ file for details), for example:
+
+ Define TestManagerRootDir "/mnt/scratch/vbox/svn/trunk/src/VBox/ValidationKit/testmanager"
+ Define VBoxBuildOutputDir "/tmp"
+ Include "${TestManagerRootDir}/apache-template-2.4.conf"
+
+ Make sure to enable cgi (a2enmod cgi && systemctl restart apache2).
+
+ - Default htpasswd file has users a user 'admin' with password 'admin' and a
+ 'test' user with password 'test'. This isn't going to get you far if
+ you've loaded something from the production server as there is typically
+ no 'admin' user in the 'Users' table there. So, you will need to add your
+ user and a throwaway password to 'misc/htpasswd-sample' using the htpasswd
+ utility.
+
+ - Try http://localhost/testmanager/ in a browser and see if it works.
+
+
+III. OS X version of the above manual setup using MacPorts:
+
+ - sudo ports install apache2 postgresql12 postgresql12-server py38-psycopg2 py38-pylint
+ sudo port select --set python python38
+ sudo port select --set python3 python38
+ sudo port select --set pylint pylint38
+
+ Note! Replace the python 38 with the most recent one you want to use. Same
+ for the 12 in relation to postgresql.
+
+ - Do what the postgresql12-server notes says, at the time of writing:
+ sudo mkdir -p /opt/local/var/db/postgresql12/defaultdb
+ sudo chown postgres:postgres /opt/local/var/db/postgresql12/defaultdb
+ sudo su postgres -c 'cd /opt/local/var/db/postgresql12 && /opt/local/lib/postgresql12/bin/initdb -D /opt/local/var/db/postgresql12/defaultdb'
+ sudo port load postgresql12-server
+
+ Note! The postgresql12-server's config is 'trust' already, so no need to
+ edit /opt/local/var/db/postgresql12/defaultdb/pg_hba.conf there. If
+ you use a different version, please check it.
+
+ - kmk load-testmanager-db
+
+ - Creating and loading a partial database dump as detailed above.
+
+ - Configure apache:
+ - sudo joe /opt/local/etc/apache2/httpd.conf:
+ - Uncomment the line "LoadModule cgi_module...".
+ - At the end of the file add (edit paths):
+ Define TestManagerRootDir "/Users/bird/coding/vbox/svn/trunk/src/VBox/ValidationKit/testmanager"
+ Define VBoxBuildOutputDir "/tmp"
+ Include "${TestManagerRootDir}/apache-template-2.4.conf"
+ - Test the config:
+ /opt/local/sbin/apachectl -t
+ - So apache will find the right python add the following to
+ /opt/local/sbin/envvars:
+ PATH=/opt/local/bin:/opt/local/sbin:$PATH
+ export PATH
+ - Load the apache service (or reload it):
+ sudo port load apache2
+ - Give apache access to read everything under TestManagerRootDir:
+ chmod -R a:rX /Users/bird/coding/vbox/svn/trunk/src/VBox/ValidationKit/testmanager
+ MYDIR=/Users/bird/coding/vbox/svn/trunk/src/VBox/ValidationKit; while [ '!' "$MYDIR" '<' "$HOME" ]; do \
+ chmod a+x "$MYDIR"; MYDIR=`dirname $MYDIR`; done
+
+ - Fix htpasswd file as detailed above and try the url (also above).
+
+
+N.B. For developing tests (../tests/), setting up a local test manager will be
+ a complete waste of time. Just run the test drivers locally.
diff --git a/src/VBox/ValidationKit/testmanager/selftest/st1-load.pgsql b/src/VBox/ValidationKit/testmanager/selftest/st1-load.pgsql
new file mode 100644
index 00000000..af4d5ace
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/selftest/st1-load.pgsql
@@ -0,0 +1,164 @@
+-- $Id: st1-load.pgsql $
+--- @file
+-- VBox Test Manager - Self Test #1 Database Load File.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+
+\set ON_ERROR_STOP 1
+\connect testmanager;
+
+BEGIN WORK;
+
+
+INSERT INTO Users (uid, sUsername, sEmail, sFullName, sLoginName)
+ VALUES (1112223331, 'st1', 'st1@example.org', 'self test #1', 'st1');
+
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (1112223331, 'st1-test1', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest1.py', '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip');
+
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test1'), 1112223331, '');
+
+INSERT INTO TestGroups (uidAuthor, sName)
+ VALUES (1112223331, 'st1-testgroup');
+
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test1'),
+ 1112223331);
+
+INSERT INTO BuildSources (uidAuthor, sName, sProduct, sBranch, asTypes, asOsArches)
+ VALUES (1112223331, 'st1-src', 'st1', 'trunk',
+ ARRAY['release', 'strict'],
+ ARRAY['win.x86', 'linux.noarch', 'solaris.amd64', 'os-agnostic.sparc64', 'os-agnostic.noarch']);
+
+INSERT INTO BuildCategories (sProduct, sBranch, sType, asOsArches)
+ VALUES ('st1', 'trunk', 'release', ARRAY['os-agnostic.noarch']);
+
+INSERT INTO Builds (uidAuthor, idBuildCategory, iRevision, sVersion, sBinaries)
+ VALUES (1112223331,
+ (SELECT idBuildCategory FROM BuildCategories WHERE sProduct = 'st1' AND sBranch = 'trunk'),
+ 1234, '1.0', '');
+
+INSERT INTO SchedGroups (uidAuthor, sName, sDescription, fEnabled, idBuildSrc)
+ VALUES (1112223331, 'st1-group', 'test test #1', TRUE,
+ (SELECT idBuildSrc FROM BuildSources WHERE sName = 'st1-src') );
+
+INSERT INTO SchedGroupMembers (idSchedGroup, idTestGroup, uidAuthor)
+ VALUES ((SELECT idSchedGroup FROM SchedGroups WHERE sName = 'st1-group'),
+ (SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ 1112223331);
+
+
+-- The second test
+
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (1112223331, 'st1-test2', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest2.py', '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip');
+
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test2'), 1112223331, '');
+
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test2'),
+ 1112223331);
+
+-- The third test
+
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (1112223331, 'st1-test3', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest3.py', '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip');
+
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test3'), 1112223331, '');
+
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test3'),
+ 1112223331);
+
+-- The fourth thru eight tests
+
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (1112223331, 'st1-test4-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test immediate-sub-tests',
+ '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip');
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test4-neg'), 1112223331, '');
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test4-neg'),
+ 1112223331);
+
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (1112223331, 'st1-test5-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test total-sub-tests',
+ '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip');
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test5-neg'), 1112223331, '');
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test5-neg'),
+ 1112223331);
+
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (1112223331, 'st1-test6-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test immediate-values',
+ '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip');
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test6-neg'), 1112223331, '');
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test6-neg'),
+ 1112223331);
+
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (1112223331, 'st1-test7-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test total-values',
+ '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip');
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test7-neg'), 1112223331, '');
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test7-neg'),
+ 1112223331);
+
+INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips)
+ VALUES (1112223331, 'st1-test8-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test immediate-messages',
+ '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip');
+INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs)
+ VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test8-neg'), 1112223331, '');
+INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor)
+ VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'),
+ (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test8-neg'),
+ 1112223331);
+
+COMMIT WORK;
+
diff --git a/src/VBox/ValidationKit/testmanager/selftest/st1-unload.pgsql b/src/VBox/ValidationKit/testmanager/selftest/st1-unload.pgsql
new file mode 100644
index 00000000..5fe797c3
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/selftest/st1-unload.pgsql
@@ -0,0 +1,87 @@
+-- $Id: st1-unload.pgsql $
+--- @file
+-- VBox Test Manager - Self Test #1 Database Unload File.
+--
+
+--
+-- Copyright (C) 2012-2023 Oracle and/or its affiliates.
+--
+-- This file is part of VirtualBox base platform packages, as
+-- available from https://www.virtualbox.org.
+--
+-- This program is free software; you can redistribute it and/or
+-- modify it under the terms of the GNU General Public License
+-- as published by the Free Software Foundation, in version 3 of the
+-- License.
+--
+-- This program is distributed in the hope that it will be useful, but
+-- WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+-- General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, see <https://www.gnu.org/licenses>.
+--
+-- The contents of this file may alternatively be used under the terms
+-- of the Common Development and Distribution License Version 1.0
+-- (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+-- in the VirtualBox distribution, in which case the provisions of the
+-- CDDL are applicable instead of those of the GPL.
+--
+-- You may elect to license modified versions of this file under the
+-- terms and conditions of either the GPL or the CDDL or both.
+--
+-- SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+--
+
+
+
+\set ON_ERROR_STOP 1
+\connect testmanager;
+
+BEGIN WORK;
+
+DELETE FROM TestBoxStatuses;
+DELETE FROM SchedQueues;
+
+DELETE FROM SchedGroupMembers WHERE uidAuthor = 1112223331;
+UPDATE TestBoxes SET idSchedGroup = 1 WHERE idSchedGroup IN ( SELECT idSchedGroup FROM SchedGroups WHERE uidAuthor = 1112223331 );
+DELETE FROM SchedGroups WHERE uidAuthor = 1112223331 OR sName = 'st1-group';
+
+UPDATE TestSets SET idTestResult = NULL
+ WHERE idTestCase IN ( SELECT idTestCase FROM TestCases WHERE uidAuthor = 1112223331 );
+
+DELETE FROM TestResultValues
+ WHERE idTestResult IN ( SELECT idTestResult FROM TestResults
+ WHERE idTestSet IN ( SELECT idTestSet FROM TestSets
+ WHERE idTestCase IN ( SELECT idTestCase FROM TestCases
+ WHERE uidAuthor = 1112223331 ) ) );
+DELETE FROM TestResultFiles
+ WHERE idTestResult IN ( SELECT idTestResult FROM TestResults
+ WHERE idTestSet IN ( SELECT idTestSet FROM TestSets
+ WHERE idTestCase IN ( SELECT idTestCase FROM TestCases
+ WHERE uidAuthor = 1112223331 ) ) );
+DELETE FROM TestResultMsgs
+ WHERE idTestResult IN ( SELECT idTestResult FROM TestResults
+ WHERE idTestSet IN ( SELECT idTestSet FROM TestSets
+ WHERE idTestCase IN ( SELECT idTestCase FROM TestCases
+ WHERE uidAuthor = 1112223331 ) ) );
+DELETE FROM TestResults
+ WHERE idTestSet IN ( SELECT idTestSet FROM TestSets
+ WHERE idTestCase IN ( SELECT idTestCase FROM TestCases WHERE uidAuthor = 1112223331 ) );
+DELETE FROM TestSets
+ WHERE idTestCase IN ( SELECT idTestCase FROM TestCases WHERE uidAuthor = 1112223331 );
+
+DELETE FROM TestCases WHERE uidAuthor = 1112223331;
+DELETE FROM TestCaseArgs WHERE uidAuthor = 1112223331;
+DELETE FROM TestGroups WHERE uidAuthor = 1112223331 OR sName = 'st1-testgroup';
+DELETE FROM TestGroupMembers WHERE uidAuthor = 1112223331;
+
+DELETE FROM BuildSources WHERE uidAuthor = 1112223331;
+DELETE FROM Builds WHERE uidAuthor = 1112223331;
+DELETE FROM BuildCategories WHERE sProduct = 'st1';
+
+DELETE FROM Users WHERE uid = 1112223331;
+
+COMMIT WORK;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk b/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk
new file mode 100644
index 00000000..5a9b58bd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk
@@ -0,0 +1,47 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+SUB_DEPTH = ../../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+
+VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py)
+VBOX_VALIDATIONKIT_PYUNITTEST_EXCLUDE += $(PATH_SUB_CURRENT)/wuihlpgraphmatplotlib.py
+
+$(evalcall def_vbox_validationkit_process_python_sources)
+$(evalcall def_vbox_validationkit_process_js_sources)
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/__init__.py b/src/VBox/ValidationKit/testmanager/webui/__init__.py
new file mode 100644
index 00000000..d00f5436
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/__init__.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# $Id: __init__.py $
+
+"""
+TestBox Script - WUI Presentation.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/template-details.html b/src/VBox/ValidationKit/testmanager/webui/template-details.html
new file mode 100644
index 00000000..e7e16c0f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/template-details.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <meta http-equiv="content-language" content="en" />
+ <meta name="language" content="en" />
+ <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" type="image/x-icon" />
+ <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" />
+ <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/details.css" rel="stylesheet" type="text/css" media="screen" />
+ <script type="text/javascript" src="htdocs/js/common.js"></script>
+ <title>@@PAGE_TITLE@@</title>
+ </head>
+
+ <body>
+ <div id="wrap">
+ <div id="head-wrap">
+ <div id="logo">
+ <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg">
+ </div>
+ <div id="header">
+ <h1>@@PAGE_TITLE@@</h1>
+ </div>
+ <div id="top-menu" class="tm-top-menu-wo-side">
+ <ul>
+ @@TOP_MENU_ITEMS@@
+ </ul>
+ </div>
+ <div id="login">
+ <p><small>
+ Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@
+ </small></p>
+ </div>
+ </div>
+
+ <div id="main">
+ @@PAGE_BODY@@
+
+ @@DEBUG@@
+ </div>
+ </div>
+ </body>
+</html>
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html b/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html
new file mode 100644
index 00000000..4e1dc0c8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <meta http-equiv="content-language" content="en" />
+ <meta name="language" content="en" />
+ <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" type="image/x-icon" />
+ <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" />
+ <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/graphwiz.css" rel="stylesheet" type="text/css" media="screen" />
+ <script type="text/javascript" src="htdocs/js/common.js"></script>
+ <script type="text/javascript" src="htdocs/js/graphwiz.js"></script>
+ <title>@@PAGE_TITLE@@</title>
+ </head>
+
+ <body>
+ <div id="wrap">
+ <div id="head-wrap">
+ <div id="logo">
+ <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg">
+ </div>
+ <div id="header">
+ <h1>@@PAGE_TITLE@@</h1>
+ </div>
+ <div id="top-menu" class="tm-top-menu-wo-side">
+ <ul>
+ @@TOP_MENU_ITEMS@@
+ </ul>
+ </div>
+ <div id="login">
+ <p><small>
+ Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@
+ </small></p>
+ </div>
+ </div>
+
+ <div id="main">
+ @@PAGE_BODY@@
+
+ @@DEBUG@@
+ </div>
+ </div>
+ </body>
+</html>
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html b/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html
new file mode 100644
index 00000000..7aa95d71
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <meta http-equiv="content-language" content="en" />
+ <meta name="language" content="en" />
+ <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" />
+ <title>@@PAGE_TITLE@@</title>
+</head>
+
+<body scroll="no">
+<div id="tooltip" class="tooltip-main">
+<div id="tooltip-inner" class="tooltip-inner">
+@@PAGE_BODY@@
+</div>
+</div>
+</body>
+</html>
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/template.html b/src/VBox/ValidationKit/testmanager/webui/template.html
new file mode 100644
index 00000000..6480c20b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/template.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <meta http-equiv="content-language" content="en" />
+ <meta name="language" content="en" />
+ <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" />
+ <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" />
+ <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" />
+ <script type="text/javascript" src="htdocs/js/common.js"></script>
+ <title>@@PAGE_TITLE@@</title>
+ </head>
+
+ <body>
+ <div id="wrap">
+ <div id="head-wrap">
+ <div id="logo">
+ <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg">
+ </div>
+ <div id="header">
+ <h1>@@PAGE_TITLE@@</h1>
+ </div>
+ <div id="login">
+ <p><small>
+ Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@
+ </small></p>
+ </div>
+ <div id="top-menu">
+ <ul>
+ @@TOP_MENU_ITEMS@@
+ </ul>
+ </div>
+ </div>
+
+ <div id="side-menu-wrap">
+ <div id="side-menu">
+ <div id="side-menu-body">
+ <form id="side-menu-form" @@SIDE_MENU_FORM_ATTRS@@>
+ <ul>
+ @@SIDE_MENU_ITEMS@@
+ </ul>
+ @@SIDE_FILTER_CONTROL@@
+ </form>
+ </div>
+ <!-- justify-content: space-between -->
+ <div id="side-footer">
+ <p>
+ VBox Test Manager<br/>@@TESTMANAGER_VERSION@@r@@TESTMANAGER_REVISION@@
+ </p>
+ <p>Copyright &copy; 2012-2023 Oracle Corporation</p>
+ </div>
+ </div>
+ </div>
+
+ <div id="main">
+ @@PAGE_BODY@@
+
+ @@DEBUG@@
+ </div>
+ </div>
+ </body>
+</html>
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py
new file mode 100755
index 00000000..ef5c7669
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py
@@ -0,0 +1,1270 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadmin.py $
+
+"""
+Test Manager Core - WUI - Admin Main page.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import cgitb;
+import sys;
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager import config;
+from testmanager.webui.wuibase import WuiDispatcherBase, WuiException
+
+
+class WuiAdmin(WuiDispatcherBase):
+ """
+ WUI Admin main page.
+ """
+
+ ## The name of the script.
+ ksScriptName = 'admin.py'
+
+ ## Number of days back.
+ ksParamDaysBack = 'cDaysBack';
+
+ ## @name Actions
+ ## @{
+ ksActionSystemLogList = 'SystemLogList'
+ ksActionSystemChangelogList = 'SystemChangelogList'
+ ksActionSystemDbDump = 'SystemDbDump'
+ ksActionSystemDbDumpDownload = 'SystemDbDumpDownload'
+
+ ksActionUserList = 'UserList'
+ ksActionUserAdd = 'UserAdd'
+ ksActionUserAddPost = 'UserAddPost'
+ ksActionUserEdit = 'UserEdit'
+ ksActionUserEditPost = 'UserEditPost'
+ ksActionUserDelPost = 'UserDelPost'
+ ksActionUserDetails = 'UserDetails'
+
+ ksActionTestBoxList = 'TestBoxList'
+ ksActionTestBoxListPost = 'TestBoxListPost'
+ ksActionTestBoxAdd = 'TestBoxAdd'
+ ksActionTestBoxAddPost = 'TestBoxAddPost'
+ ksActionTestBoxEdit = 'TestBoxEdit'
+ ksActionTestBoxEditPost = 'TestBoxEditPost'
+ ksActionTestBoxDetails = 'TestBoxDetails'
+ ksActionTestBoxRemovePost = 'TestBoxRemove'
+ ksActionTestBoxesRegenQueues = 'TestBoxesRegenQueues';
+
+ ksActionTestCaseList = 'TestCaseList'
+ ksActionTestCaseAdd = 'TestCaseAdd'
+ ksActionTestCaseAddPost = 'TestCaseAddPost'
+ ksActionTestCaseClone = 'TestCaseClone'
+ ksActionTestCaseDetails = 'TestCaseDetails'
+ ksActionTestCaseEdit = 'TestCaseEdit'
+ ksActionTestCaseEditPost = 'TestCaseEditPost'
+ ksActionTestCaseDoRemove = 'TestCaseDoRemove'
+
+ ksActionGlobalRsrcShowAll = 'GlobalRsrcShowAll'
+ ksActionGlobalRsrcShowAdd = 'GlobalRsrcShowAdd'
+ ksActionGlobalRsrcShowEdit = 'GlobalRsrcShowEdit'
+ ksActionGlobalRsrcAdd = 'GlobalRsrcAddPost'
+ ksActionGlobalRsrcEdit = 'GlobalRsrcEditPost'
+ ksActionGlobalRsrcDel = 'GlobalRsrcDelPost'
+
+ ksActionBuildList = 'BuildList'
+ ksActionBuildAdd = 'BuildAdd'
+ ksActionBuildAddPost = 'BuildAddPost'
+ ksActionBuildClone = 'BuildClone'
+ ksActionBuildDetails = 'BuildDetails'
+ ksActionBuildDoRemove = 'BuildDoRemove'
+ ksActionBuildEdit = 'BuildEdit'
+ ksActionBuildEditPost = 'BuildEditPost'
+
+ ksActionBuildBlacklist = 'BuildBlacklist';
+ ksActionBuildBlacklistAdd = 'BuildBlacklistAdd';
+ ksActionBuildBlacklistAddPost = 'BuildBlacklistAddPost';
+ ksActionBuildBlacklistClone = 'BuildBlacklistClone';
+ ksActionBuildBlacklistDetails = 'BuildBlacklistDetails';
+ ksActionBuildBlacklistDoRemove = 'BuildBlacklistDoRemove';
+ ksActionBuildBlacklistEdit = 'BuildBlacklistEdit';
+ ksActionBuildBlacklistEditPost = 'BuildBlacklistEditPost';
+
+ ksActionFailureCategoryList = 'FailureCategoryList';
+ ksActionFailureCategoryAdd = 'FailureCategoryAdd';
+ ksActionFailureCategoryAddPost = 'FailureCategoryAddPost';
+ ksActionFailureCategoryDetails = 'FailureCategoryDetails';
+ ksActionFailureCategoryDoRemove = 'FailureCategoryDoRemove';
+ ksActionFailureCategoryEdit = 'FailureCategoryEdit';
+ ksActionFailureCategoryEditPost = 'FailureCategoryEditPost';
+
+ ksActionFailureReasonList = 'FailureReasonList'
+ ksActionFailureReasonAdd = 'FailureReasonAdd'
+ ksActionFailureReasonAddPost = 'FailureReasonAddPost'
+ ksActionFailureReasonDetails = 'FailureReasonDetails'
+ ksActionFailureReasonDoRemove = 'FailureReasonDoRemove'
+ ksActionFailureReasonEdit = 'FailureReasonEdit'
+ ksActionFailureReasonEditPost = 'FailureReasonEditPost'
+
+ ksActionBuildSrcList = 'BuildSrcList'
+ ksActionBuildSrcAdd = 'BuildSrcAdd'
+ ksActionBuildSrcAddPost = 'BuildSrcAddPost'
+ ksActionBuildSrcClone = 'BuildSrcClone'
+ ksActionBuildSrcDetails = 'BuildSrcDetails'
+ ksActionBuildSrcEdit = 'BuildSrcEdit'
+ ksActionBuildSrcEditPost = 'BuildSrcEditPost'
+ ksActionBuildSrcDoRemove = 'BuildSrcDoRemove'
+
+ ksActionBuildCategoryList = 'BuildCategoryList'
+ ksActionBuildCategoryAdd = 'BuildCategoryAdd'
+ ksActionBuildCategoryAddPost = 'BuildCategoryAddPost'
+ ksActionBuildCategoryClone = 'BuildCategoryClone';
+ ksActionBuildCategoryDetails = 'BuildCategoryDetails';
+ ksActionBuildCategoryDoRemove = 'BuildCategoryDoRemove';
+
+ ksActionTestGroupList = 'TestGroupList'
+ ksActionTestGroupAdd = 'TestGroupAdd'
+ ksActionTestGroupAddPost = 'TestGroupAddPost'
+ ksActionTestGroupClone = 'TestGroupClone'
+ ksActionTestGroupDetails = 'TestGroupDetails'
+ ksActionTestGroupDoRemove = 'TestGroupDoRemove'
+ ksActionTestGroupEdit = 'TestGroupEdit'
+ ksActionTestGroupEditPost = 'TestGroupEditPost'
+ ksActionTestCfgRegenQueues = 'TestCfgRegenQueues'
+
+ ksActionSchedGroupList = 'SchedGroupList'
+ ksActionSchedGroupAdd = 'SchedGroupAdd';
+ ksActionSchedGroupAddPost = 'SchedGroupAddPost';
+ ksActionSchedGroupClone = 'SchedGroupClone';
+ ksActionSchedGroupDetails = 'SchedGroupDetails';
+ ksActionSchedGroupDoRemove = 'SchedGroupDel';
+ ksActionSchedGroupEdit = 'SchedGroupEdit';
+ ksActionSchedGroupEditPost = 'SchedGroupEditPost';
+ ksActionSchedQueueList = 'SchedQueueList';
+ ## @}
+
+ def __init__(self, oSrvGlue): # pylint: disable=too-many-locals,too-many-statements
+ WuiDispatcherBase.__init__(self, oSrvGlue, self.ksScriptName);
+ self._sTemplate = 'template.html';
+
+
+ #
+ # System actions.
+ #
+ self._dDispatch[self.ksActionSystemChangelogList] = self._actionSystemChangelogList;
+ self._dDispatch[self.ksActionSystemLogList] = self._actionSystemLogList;
+ self._dDispatch[self.ksActionSystemDbDump] = self._actionSystemDbDump;
+ self._dDispatch[self.ksActionSystemDbDumpDownload] = self._actionSystemDbDumpDownload;
+
+ #
+ # User Account actions.
+ #
+ self._dDispatch[self.ksActionUserList] = self._actionUserList;
+ self._dDispatch[self.ksActionUserAdd] = self._actionUserAdd;
+ self._dDispatch[self.ksActionUserEdit] = self._actionUserEdit;
+ self._dDispatch[self.ksActionUserAddPost] = self._actionUserAddPost;
+ self._dDispatch[self.ksActionUserEditPost] = self._actionUserEditPost;
+ self._dDispatch[self.ksActionUserDetails] = self._actionUserDetails;
+ self._dDispatch[self.ksActionUserDelPost] = self._actionUserDelPost;
+
+ #
+ # TestBox actions.
+ #
+ self._dDispatch[self.ksActionTestBoxList] = self._actionTestBoxList;
+ self._dDispatch[self.ksActionTestBoxListPost] = self._actionTestBoxListPost;
+ self._dDispatch[self.ksActionTestBoxAdd] = self._actionTestBoxAdd;
+ self._dDispatch[self.ksActionTestBoxAddPost] = self._actionTestBoxAddPost;
+ self._dDispatch[self.ksActionTestBoxDetails] = self._actionTestBoxDetails;
+ self._dDispatch[self.ksActionTestBoxEdit] = self._actionTestBoxEdit;
+ self._dDispatch[self.ksActionTestBoxEditPost] = self._actionTestBoxEditPost;
+ self._dDispatch[self.ksActionTestBoxRemovePost] = self._actionTestBoxRemovePost;
+ self._dDispatch[self.ksActionTestBoxesRegenQueues] = self._actionRegenQueuesCommon;
+
+ #
+ # Test Case actions.
+ #
+ self._dDispatch[self.ksActionTestCaseList] = self._actionTestCaseList;
+ self._dDispatch[self.ksActionTestCaseAdd] = self._actionTestCaseAdd;
+ self._dDispatch[self.ksActionTestCaseAddPost] = self._actionTestCaseAddPost;
+ self._dDispatch[self.ksActionTestCaseClone] = self._actionTestCaseClone;
+ self._dDispatch[self.ksActionTestCaseDetails] = self._actionTestCaseDetails;
+ self._dDispatch[self.ksActionTestCaseEdit] = self._actionTestCaseEdit;
+ self._dDispatch[self.ksActionTestCaseEditPost] = self._actionTestCaseEditPost;
+ self._dDispatch[self.ksActionTestCaseDoRemove] = self._actionTestCaseDoRemove;
+
+ #
+ # Global Resource actions
+ #
+ self._dDispatch[self.ksActionGlobalRsrcShowAll] = self._actionGlobalRsrcShowAll;
+ self._dDispatch[self.ksActionGlobalRsrcShowAdd] = self._actionGlobalRsrcShowAdd;
+ self._dDispatch[self.ksActionGlobalRsrcShowEdit] = self._actionGlobalRsrcShowEdit;
+ self._dDispatch[self.ksActionGlobalRsrcAdd] = self._actionGlobalRsrcAdd;
+ self._dDispatch[self.ksActionGlobalRsrcEdit] = self._actionGlobalRsrcEdit;
+ self._dDispatch[self.ksActionGlobalRsrcDel] = self._actionGlobalRsrcDel;
+
+ #
+ # Build Source actions
+ #
+ self._dDispatch[self.ksActionBuildSrcList] = self._actionBuildSrcList;
+ self._dDispatch[self.ksActionBuildSrcAdd] = self._actionBuildSrcAdd;
+ self._dDispatch[self.ksActionBuildSrcAddPost] = self._actionBuildSrcAddPost;
+ self._dDispatch[self.ksActionBuildSrcClone] = self._actionBuildSrcClone;
+ self._dDispatch[self.ksActionBuildSrcDetails] = self._actionBuildSrcDetails;
+ self._dDispatch[self.ksActionBuildSrcDoRemove] = self._actionBuildSrcDoRemove;
+ self._dDispatch[self.ksActionBuildSrcEdit] = self._actionBuildSrcEdit;
+ self._dDispatch[self.ksActionBuildSrcEditPost] = self._actionBuildSrcEditPost;
+
+ #
+ # Build actions
+ #
+ self._dDispatch[self.ksActionBuildList] = self._actionBuildList;
+ self._dDispatch[self.ksActionBuildAdd] = self._actionBuildAdd;
+ self._dDispatch[self.ksActionBuildAddPost] = self._actionBuildAddPost;
+ self._dDispatch[self.ksActionBuildClone] = self._actionBuildClone;
+ self._dDispatch[self.ksActionBuildDetails] = self._actionBuildDetails;
+ self._dDispatch[self.ksActionBuildDoRemove] = self._actionBuildDoRemove;
+ self._dDispatch[self.ksActionBuildEdit] = self._actionBuildEdit;
+ self._dDispatch[self.ksActionBuildEditPost] = self._actionBuildEditPost;
+
+ #
+ # Build Black List actions
+ #
+ self._dDispatch[self.ksActionBuildBlacklist] = self._actionBuildBlacklist;
+ self._dDispatch[self.ksActionBuildBlacklistAdd] = self._actionBuildBlacklistAdd;
+ self._dDispatch[self.ksActionBuildBlacklistAddPost] = self._actionBuildBlacklistAddPost;
+ self._dDispatch[self.ksActionBuildBlacklistClone] = self._actionBuildBlacklistClone;
+ self._dDispatch[self.ksActionBuildBlacklistDetails] = self._actionBuildBlacklistDetails;
+ self._dDispatch[self.ksActionBuildBlacklistDoRemove] = self._actionBuildBlacklistDoRemove;
+ self._dDispatch[self.ksActionBuildBlacklistEdit] = self._actionBuildBlacklistEdit;
+ self._dDispatch[self.ksActionBuildBlacklistEditPost] = self._actionBuildBlacklistEditPost;
+
+ #
+ # Failure Category actions
+ #
+ self._dDispatch[self.ksActionFailureCategoryList] = self._actionFailureCategoryList;
+ self._dDispatch[self.ksActionFailureCategoryAdd] = self._actionFailureCategoryAdd;
+ self._dDispatch[self.ksActionFailureCategoryAddPost] = self._actionFailureCategoryAddPost;
+ self._dDispatch[self.ksActionFailureCategoryDetails] = self._actionFailureCategoryDetails;
+ self._dDispatch[self.ksActionFailureCategoryDoRemove] = self._actionFailureCategoryDoRemove;
+ self._dDispatch[self.ksActionFailureCategoryEdit] = self._actionFailureCategoryEdit;
+ self._dDispatch[self.ksActionFailureCategoryEditPost] = self._actionFailureCategoryEditPost;
+
+ #
+ # Failure Reason actions
+ #
+ self._dDispatch[self.ksActionFailureReasonList] = self._actionFailureReasonList;
+ self._dDispatch[self.ksActionFailureReasonAdd] = self._actionFailureReasonAdd;
+ self._dDispatch[self.ksActionFailureReasonAddPost] = self._actionFailureReasonAddPost;
+ self._dDispatch[self.ksActionFailureReasonDetails] = self._actionFailureReasonDetails;
+ self._dDispatch[self.ksActionFailureReasonDoRemove] = self._actionFailureReasonDoRemove;
+ self._dDispatch[self.ksActionFailureReasonEdit] = self._actionFailureReasonEdit;
+ self._dDispatch[self.ksActionFailureReasonEditPost] = self._actionFailureReasonEditPost;
+
+ #
+ # Build Category actions
+ #
+ self._dDispatch[self.ksActionBuildCategoryList] = self._actionBuildCategoryList;
+ self._dDispatch[self.ksActionBuildCategoryAdd] = self._actionBuildCategoryAdd;
+ self._dDispatch[self.ksActionBuildCategoryAddPost] = self._actionBuildCategoryAddPost;
+ self._dDispatch[self.ksActionBuildCategoryClone] = self._actionBuildCategoryClone;
+ self._dDispatch[self.ksActionBuildCategoryDetails] = self._actionBuildCategoryDetails;
+ self._dDispatch[self.ksActionBuildCategoryDoRemove] = self._actionBuildCategoryDoRemove;
+
+ #
+ # Test Group actions
+ #
+ self._dDispatch[self.ksActionTestGroupList] = self._actionTestGroupList;
+ self._dDispatch[self.ksActionTestGroupAdd] = self._actionTestGroupAdd;
+ self._dDispatch[self.ksActionTestGroupAddPost] = self._actionTestGroupAddPost;
+ self._dDispatch[self.ksActionTestGroupClone] = self._actionTestGroupClone;
+ self._dDispatch[self.ksActionTestGroupDetails] = self._actionTestGroupDetails;
+ self._dDispatch[self.ksActionTestGroupEdit] = self._actionTestGroupEdit;
+ self._dDispatch[self.ksActionTestGroupEditPost] = self._actionTestGroupEditPost;
+ self._dDispatch[self.ksActionTestGroupDoRemove] = self._actionTestGroupDoRemove;
+ self._dDispatch[self.ksActionTestCfgRegenQueues] = self._actionRegenQueuesCommon;
+
+ #
+ # Scheduling Group and Queue actions
+ #
+ self._dDispatch[self.ksActionSchedGroupList] = self._actionSchedGroupList;
+ self._dDispatch[self.ksActionSchedGroupAdd] = self._actionSchedGroupAdd;
+ self._dDispatch[self.ksActionSchedGroupClone] = self._actionSchedGroupClone;
+ self._dDispatch[self.ksActionSchedGroupDetails] = self._actionSchedGroupDetails;
+ self._dDispatch[self.ksActionSchedGroupEdit] = self._actionSchedGroupEdit;
+ self._dDispatch[self.ksActionSchedGroupAddPost] = self._actionSchedGroupAddPost;
+ self._dDispatch[self.ksActionSchedGroupEditPost] = self._actionSchedGroupEditPost;
+ self._dDispatch[self.ksActionSchedGroupDoRemove] = self._actionSchedGroupDoRemove;
+ self._dDispatch[self.ksActionSchedQueueList] = self._actionSchedQueueList;
+
+
+ #
+ # Menus
+ #
+ self._aaoMenus = \
+ [
+ [
+ 'Builds', self._sActionUrlBase + self.ksActionBuildList,
+ [
+ [ 'Builds', self._sActionUrlBase + self.ksActionBuildList, False ],
+ [ 'Blacklist', self._sActionUrlBase + self.ksActionBuildBlacklist, False ],
+ [ 'Build sources', self._sActionUrlBase + self.ksActionBuildSrcList, False ],
+ [ 'Build categories', self._sActionUrlBase + self.ksActionBuildCategoryList, False ],
+ [ 'New build', self._sActionUrlBase + self.ksActionBuildAdd, True ],
+ [ 'New blacklisting', self._sActionUrlBase + self.ksActionBuildBlacklistAdd, True ],
+ [ 'New build source', self._sActionUrlBase + self.ksActionBuildSrcAdd, True ],
+ [ 'New build category', self._sActionUrlBase + self.ksActionBuildCategoryAdd, True ],
+ ]
+ ],
+ [
+ 'Failure Reasons', self._sActionUrlBase + self.ksActionFailureReasonList,
+ [
+ [ 'Failure categories', self._sActionUrlBase + self.ksActionFailureCategoryList, False ],
+ [ 'Failure reasons', self._sActionUrlBase + self.ksActionFailureReasonList, False ],
+ [ 'New failure category', self._sActionUrlBase + self.ksActionFailureCategoryAdd, True ],
+ [ 'New failure reason', self._sActionUrlBase + self.ksActionFailureReasonAdd, True ],
+ ]
+ ],
+ [
+ 'System', self._sActionUrlBase + self.ksActionSystemChangelogList,
+ [
+ [ 'Changelog', self._sActionUrlBase + self.ksActionSystemChangelogList, False ],
+ [ 'System log', self._sActionUrlBase + self.ksActionSystemLogList, False ],
+ [ 'Partial DB Dump', self._sActionUrlBase + self.ksActionSystemDbDump, False ],
+ [ 'User accounts', self._sActionUrlBase + self.ksActionUserList, False ],
+ [ 'New user', self._sActionUrlBase + self.ksActionUserAdd, True ],
+ ]
+ ],
+ [
+ 'Testboxes', self._sActionUrlBase + self.ksActionTestBoxList,
+ [
+ [ 'Testboxes', self._sActionUrlBase + self.ksActionTestBoxList, False ],
+ [ 'Scheduling groups', self._sActionUrlBase + self.ksActionSchedGroupList, False ],
+ [ 'New testbox', self._sActionUrlBase + self.ksActionTestBoxAdd, True ],
+ [ 'New scheduling group', self._sActionUrlBase + self.ksActionSchedGroupAdd, True ],
+ [ 'View scheduling queues', self._sActionUrlBase + self.ksActionSchedQueueList, False ],
+ [ 'Regenerate all scheduling queues', self._sActionUrlBase + self.ksActionTestBoxesRegenQueues, True ],
+ ]
+ ],
+ [
+ 'Test Config', self._sActionUrlBase + self.ksActionTestGroupList,
+ [
+ [ 'Test cases', self._sActionUrlBase + self.ksActionTestCaseList, False ],
+ [ 'Test groups', self._sActionUrlBase + self.ksActionTestGroupList, False ],
+ [ 'Global resources', self._sActionUrlBase + self.ksActionGlobalRsrcShowAll, False ],
+ [ 'New test case', self._sActionUrlBase + self.ksActionTestCaseAdd, True ],
+ [ 'New test group', self._sActionUrlBase + self.ksActionTestGroupAdd, True ],
+ [ 'New global resource', self._sActionUrlBase + self.ksActionGlobalRsrcShowAdd, True ],
+ [ 'Regenerate all scheduling queues', self._sActionUrlBase + self.ksActionTestCfgRegenQueues, True ],
+ ]
+ ],
+ [
+ '> Test Results', 'index.py?' + webutils.encodeUrlParams(self._dDbgParams), []
+ ],
+ ];
+
+
+ def _actionDefault(self):
+ """Show the default admin page."""
+ self._sAction = self.ksActionTestBoxList;
+ from testmanager.core.testbox import TestBoxLogic;
+ from testmanager.webui.wuiadmintestbox import WuiTestBoxList;
+ return self._actionGenericListing(TestBoxLogic, WuiTestBoxList);
+
+ def _actionGenericDoDelOld(self, oCoreObjectLogic, sCoreObjectIdFieldName, sRedirectAction):
+ """
+ Delete entry (using oLogicType.remove).
+
+ @param oCoreObjectLogic A *Logic class
+
+ @param sCoreObjectIdFieldName Name of HTTP POST variable that
+ contains object ID information
+
+ @param sRedirectAction An action for redirect user to
+ in case of operation success
+ """
+ iCoreDataObjectId = self.getStringParam(sCoreObjectIdFieldName) # STRING?!?!
+ self._checkForUnknownParameters()
+
+ try:
+ self._sPageTitle = None
+ self._sPageBody = None
+ self._sRedirectTo = self._sActionUrlBase + sRedirectAction
+ return oCoreObjectLogic(self._oDb).remove(self._oCurUser.uid, iCoreDataObjectId)
+ except Exception as oXcpt:
+ self._oDb.rollback();
+ self._sPageTitle = 'Unable to delete record'
+ self._sPageBody = str(oXcpt);
+ if config.g_kfDebugDbXcpt:
+ self._sPageBody += cgitb.html(sys.exc_info());
+ self._sRedirectTo = None
+
+ return False
+
+
+ #
+ # System Category.
+ #
+
+ # System wide changelog actions.
+
+ def _actionSystemChangelogList(self):
+ """ Action handler. """
+ from testmanager.core.systemchangelog import SystemChangelogLogic;
+ from testmanager.webui.wuiadminsystemchangelog import WuiAdminSystemChangelogList;
+
+ tsEffective = self.getEffectiveDateParam();
+ cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 384);
+ iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0);
+ cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = 1, iMax = 366, iDefault = 14);
+ self._checkForUnknownParameters();
+
+ aoEntries = SystemChangelogLogic(self._oDb).fetchForListingEx(iPage * cItemsPerPage, cItemsPerPage + 1,
+ tsEffective, cDaysBack);
+ oContent = WuiAdminSystemChangelogList(aoEntries, iPage, cItemsPerPage, tsEffective,
+ cDaysBack = cDaysBack, fnDPrint = self._oSrvGlue.dprint, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
+ # System Log actions.
+
+ def _actionSystemLogList(self):
+ """ Action wrapper. """
+ from testmanager.core.systemlog import SystemLogLogic;
+ from testmanager.webui.wuiadminsystemlog import WuiAdminSystemLogList;
+ return self._actionGenericListing(SystemLogLogic, WuiAdminSystemLogList)
+
+ def _actionSystemDbDump(self):
+ """ Action handler. """
+ from testmanager.webui.wuiadminsystemdbdump import WuiAdminSystemDbDumpForm;
+
+ cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = config.g_kcTmDbDumpMinDays,
+ iMax = config.g_kcTmDbDumpMaxDays, iDefault = config.g_kcTmDbDumpDefaultDays);
+ self._checkForUnknownParameters();
+
+ oContent = WuiAdminSystemDbDumpForm(cDaysBack, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.showForm();
+ return True;
+
+ def _actionSystemDbDumpDownload(self):
+ """ Action handler. """
+ import datetime;
+ import os;
+
+ cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = config.g_kcTmDbDumpMinDays,
+ iMax = config.g_kcTmDbDumpMaxDays, iDefault = config.g_kcTmDbDumpDefaultDays);
+ self._checkForUnknownParameters();
+
+ #
+ # Generate the dump.
+ #
+ # We generate a file name that's unique to a user is smart enough to only
+ # issue one of these requests at the time. This also makes sure we won't
+ # waste too much space should this code get interrupted and rerun.
+ #
+ oFile = None;
+ oNow = datetime.datetime.utcnow();
+ sOutFile = config.g_ksTmDbDumpOutFileTmpl % (self._oCurUser.uid,);
+ sTmpFile = config.g_ksTmDbDumpTmpFileTmpl % (self._oCurUser.uid,);
+ sScript = os.path.join(config.g_ksTestManagerDir, 'db', 'partial-db-dump.py');
+ try:
+ (iExitCode, sStdOut, sStdErr) = utils.processOutputUnchecked([ sScript,
+ '--days-to-dump', str(cDaysBack),
+ '-f', sOutFile,
+ '-t', sTmpFile,
+ ]);
+ if iExitCode != 0:
+ raise Exception('iExitCode=%s\n--- stderr ---\n%s\n--- stdout ---\n%s' % (iExitCode, sStdOut, sStdErr,));
+
+ #
+ # Open and send the dump.
+ #
+ oFile = open(sOutFile, 'rb'); # pylint: disable=consider-using-with
+ cbFile = os.fstat(oFile.fileno()).st_size;
+
+ self._oSrvGlue.setHeaderField('Content-Type', 'application/zip');
+ self._oSrvGlue.setHeaderField('Content-Disposition',
+ oNow.strftime('attachment; filename="partial-db-dump-%Y-%m-%dT%H-%M-%S.zip"'));
+ self._oSrvGlue.setHeaderField('Content-Length', str(cbFile));
+
+ while True:
+ abChunk = oFile.read(262144);
+ if not abChunk:
+ break;
+ self._oSrvGlue.writeRaw(abChunk);
+
+ finally:
+ # Delete the file to save space.
+ if oFile:
+ try: oFile.close();
+ except: pass;
+ utils.noxcptDeleteFile(sOutFile);
+ utils.noxcptDeleteFile(sTmpFile);
+ return self.ksDispatchRcAllDone;
+
+
+ # User Account actions.
+
+ def _actionUserList(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountLogic;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccountList;
+ return self._actionGenericListing(UserAccountLogic, WuiUserAccountList)
+
+ def _actionUserAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormAdd(UserAccountData, WuiUserAccount)
+
+ def _actionUserDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData, UserAccountLogic;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormDetails(UserAccountData, UserAccountLogic, WuiUserAccount, 'uid');
+
+ def _actionUserEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormEdit(UserAccountData, WuiUserAccount, UserAccountData.ksParam_uid);
+
+ def _actionUserAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData, UserAccountLogic;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormAddPost(UserAccountData, UserAccountLogic, WuiUserAccount, self.ksActionUserList)
+
+ def _actionUserEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData, UserAccountLogic;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormEditPost(UserAccountData, UserAccountLogic, WuiUserAccount, self.ksActionUserList)
+
+ def _actionUserDelPost(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData, UserAccountLogic;
+ return self._actionGenericDoRemove(UserAccountLogic, UserAccountData.ksParam_uid, self.ksActionUserList)
+
+
+ #
+ # TestBox & Scheduling Category.
+ #
+
+ def _actionTestBoxList(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxLogic
+ from testmanager.webui.wuiadmintestbox import WuiTestBoxList;
+ return self._actionGenericListing(TestBoxLogic, WuiTestBoxList);
+
+ def _actionTestBoxListPost(self):
+ """Actions on a list of testboxes."""
+ from testmanager.core.testbox import TestBoxData, TestBoxLogic
+ from testmanager.webui.wuiadmintestbox import WuiTestBoxList;
+
+ # Parameters.
+ aidTestBoxes = self.getListOfIntParams(TestBoxData.ksParam_idTestBox, iMin = 1, aiDefaults = []);
+ sListAction = self.getStringParam(self.ksParamListAction);
+ if sListAction in [asDesc[0] for asDesc in WuiTestBoxList.kasTestBoxActionDescs]:
+ idAction = None;
+ else:
+ asActionPrefixes = [ 'setgroup-', ];
+ i = 0;
+ while i < len(asActionPrefixes) and not sListAction.startswith(asActionPrefixes[i]):
+ i += 1;
+ if i >= len(asActionPrefixes):
+ raise WuiException('Parameter "%s" has an invalid value: "%s"' % (self.ksParamListAction, sListAction,));
+ idAction = sListAction[len(asActionPrefixes[i]):];
+ if not idAction.isdigit():
+ raise WuiException('Parameter "%s" has an invalid value: "%s"' % (self.ksParamListAction, sListAction,));
+ idAction = int(idAction);
+ sListAction = sListAction[:len(asActionPrefixes[i]) - 1];
+ self._checkForUnknownParameters();
+
+
+ # Take action.
+ if sListAction == 'none':
+ pass;
+ else:
+ oLogic = TestBoxLogic(self._oDb);
+ aoTestBoxes = []
+ for idTestBox in aidTestBoxes:
+ aoTestBoxes.append(TestBoxData().initFromDbWithId(self._oDb, idTestBox));
+
+ if sListAction in [ 'enable', 'disable' ]:
+ fEnable = sListAction == 'enable';
+ for oTestBox in aoTestBoxes:
+ if oTestBox.fEnabled != fEnable:
+ oTestBox.fEnabled = fEnable;
+ oLogic.editEntry(oTestBox, self._oCurUser.uid, fCommit = False);
+ else:
+ for oTestBox in aoTestBoxes:
+ if oTestBox.enmPendingCmd != sListAction:
+ oLogic.setCommand(oTestBox.idTestBox, oTestBox.enmPendingCmd, sListAction, self._oCurUser.uid,
+ fCommit = False);
+ self._oDb.commit();
+
+ # Re-display the list.
+ self._sPageTitle = None;
+ self._sPageBody = None;
+ self._sRedirectTo = self._sActionUrlBase + self.ksActionTestBoxList;
+ return True;
+
+ def _actionTestBoxAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormAdd(TestBoxDataEx, WuiTestBox);
+
+ def _actionTestBoxAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormAddPost(TestBoxDataEx, TestBoxLogic, WuiTestBox, self.ksActionTestBoxList);
+
+ def _actionTestBoxDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormDetails(TestBoxDataEx, TestBoxLogic, WuiTestBox, 'idTestBox', 'idGenTestBox');
+
+ def _actionTestBoxEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormEdit(TestBoxDataEx, WuiTestBox, TestBoxDataEx.ksParam_idTestBox);
+
+ def _actionTestBoxEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormEditPost(TestBoxDataEx, TestBoxLogic,WuiTestBox, self.ksActionTestBoxList);
+
+ def _actionTestBoxRemovePost(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxData, TestBoxLogic;
+ return self._actionGenericDoRemove(TestBoxLogic, TestBoxData.ksParam_idTestBox, self.ksActionTestBoxList);
+
+
+ # Scheduling Group actions
+
+ def _actionSchedGroupList(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupLogic;
+ from testmanager.webui.wuiadminschedgroup import WuiAdminSchedGroupList;
+ return self._actionGenericListing(SchedGroupLogic, WuiAdminSchedGroupList);
+
+ def _actionSchedGroupAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormAdd(SchedGroupDataEx, WuiSchedGroup);
+
+ def _actionSchedGroupClone(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormClone( SchedGroupDataEx, WuiSchedGroup, 'idSchedGroup');
+
+ def _actionSchedGroupDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormDetails(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, 'idSchedGroup');
+
+ def _actionSchedGroupEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormEdit(SchedGroupDataEx, WuiSchedGroup, SchedGroupDataEx.ksParam_idSchedGroup);
+
+ def _actionSchedGroupAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormAddPost(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, self.ksActionSchedGroupList);
+
+ def _actionSchedGroupEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormEditPost(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, self.ksActionSchedGroupList);
+
+ def _actionSchedGroupDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic;
+ return self._actionGenericDoRemove(SchedGroupLogic, SchedGroupData.ksParam_idSchedGroup, self.ksActionSchedGroupList)
+
+ def _actionSchedQueueList(self):
+ """ Action wrapper. """
+ from testmanager.core.schedqueue import SchedQueueLogic;
+ from testmanager.webui.wuiadminschedqueue import WuiAdminSchedQueueList;
+ return self._actionGenericListing(SchedQueueLogic, WuiAdminSchedQueueList);
+
+ def _actionRegenQueuesCommon(self):
+ """
+ Common code for ksActionTestBoxesRegenQueues and ksActionTestCfgRegenQueues.
+
+ Too lazy to put this in some separate place right now.
+ """
+ from testmanager.core.schedgroup import SchedGroupLogic;
+ from testmanager.core.schedulerbase import SchedulerBase;
+
+ self._checkForUnknownParameters();
+ ## @todo should also be changed to a POST with a confirmation dialog preceeding it.
+
+ self._sPageTitle = 'Regenerate All Scheduling Queues';
+ if not self.isReadOnlyUser():
+ self._sPageBody = '';
+ aoGroups = SchedGroupLogic(self._oDb).getAll();
+ for oGroup in aoGroups:
+ self._sPageBody += '<h3>%s (ID %#d)</h3>' % (webutils.escapeElem(oGroup.sName), oGroup.idSchedGroup);
+ try:
+ (aoErrors, asMessages) = SchedulerBase.recreateQueue(self._oDb, self._oCurUser.uid, oGroup.idSchedGroup, 2);
+ except Exception as oXcpt:
+ self._oDb.rollback();
+ self._sPageBody += '<p>SchedulerBase.recreateQueue threw an exception: %s</p>' \
+ % (webutils.escapeElem(str(oXcpt)),);
+ self._sPageBody += cgitb.html(sys.exc_info());
+ else:
+ if not aoErrors:
+ self._sPageBody += '<p>Successfully regenerated.</p>';
+ else:
+ for oError in aoErrors:
+ if oError[1] is None:
+ self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),);
+ ## @todo links.
+ #elif isinstance(oError[1], TestGroupData):
+ # self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),);
+ #elif isinstance(oError[1], TestGroupCase):
+ # self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),);
+ else:
+ self._sPageBody += '<p>%s. [Cannot link to %s]</p>' \
+ % (webutils.escapeElem(oError[0]), webutils.escapeElem(str(oError[1])),);
+ for sMsg in asMessages:
+ self._sPageBody += '<p>%s<p>\n' % (webutils.escapeElem(sMsg),);
+
+ # Remove leftovers from deleted scheduling groups.
+ self._sPageBody += '<h3>Cleanups</h3>\n';
+ cOrphans = SchedulerBase.cleanUpOrphanedQueues(self._oDb);
+ self._sPageBody += '<p>Removed %s orphaned (deleted) queue%s.<p>\n' % (cOrphans, '' if cOrphans == 1 else 's', );
+ else:
+ self._sPageBody = webutils.escapeElem('%s is a read only user and may not regenerate the scheduling queues!'
+ % (self._oCurUser.sUsername,));
+ return True;
+
+
+
+ #
+ # Test Config Category.
+ #
+
+ # Test Cases
+
+ def _actionTestCaseList(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseLogic;
+ from testmanager.webui.wuiadmintestcase import WuiTestCaseList;
+ return self._actionGenericListing(TestCaseLogic, WuiTestCaseList);
+
+ def _actionTestCaseAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormAdd(TestCaseDataEx, WuiTestCase);
+
+ def _actionTestCaseAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormAddPost(TestCaseDataEx, TestCaseLogic, WuiTestCase, self.ksActionTestCaseList);
+
+ def _actionTestCaseClone(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormClone( TestCaseDataEx, WuiTestCase, 'idTestCase', 'idGenTestCase');
+
+ def _actionTestCaseDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormDetails(TestCaseDataEx, TestCaseLogic, WuiTestCase, 'idTestCase', 'idGenTestCase');
+
+ def _actionTestCaseEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormEdit(TestCaseDataEx, WuiTestCase, TestCaseDataEx.ksParam_idTestCase);
+
+ def _actionTestCaseEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormEditPost(TestCaseDataEx, TestCaseLogic, WuiTestCase, self.ksActionTestCaseList);
+
+ def _actionTestCaseDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseData, TestCaseLogic;
+ return self._actionGenericDoRemove(TestCaseLogic, TestCaseData.ksParam_idTestCase, self.ksActionTestCaseList);
+
+ # Test Group actions
+
+ def _actionTestGroupList(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupLogic;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroupList;
+ return self._actionGenericListing(TestGroupLogic, WuiTestGroupList);
+ def _actionTestGroupAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormAdd(TestGroupDataEx, WuiTestGroup);
+ def _actionTestGroupAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormAddPost(TestGroupDataEx, TestGroupLogic, WuiTestGroup, self.ksActionTestGroupList);
+ def _actionTestGroupClone(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormClone(TestGroupDataEx, WuiTestGroup, 'idTestGroup');
+ def _actionTestGroupDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormDetails(TestGroupDataEx, TestGroupLogic, WuiTestGroup, 'idTestGroup');
+ def _actionTestGroupEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormEdit(TestGroupDataEx, WuiTestGroup, TestGroupDataEx.ksParam_idTestGroup);
+ def _actionTestGroupEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormEditPost(TestGroupDataEx, TestGroupLogic, WuiTestGroup, self.ksActionTestGroupList);
+ def _actionTestGroupDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic;
+ return self._actionGenericDoRemove(TestGroupLogic, TestGroupDataEx.ksParam_idTestGroup, self.ksActionTestGroupList)
+
+
+ # Global Resources
+
+ def _actionGlobalRsrcShowAll(self):
+ """ Action wrapper. """
+ from testmanager.core.globalresource import GlobalResourceLogic;
+ from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResourceList;
+ return self._actionGenericListing(GlobalResourceLogic, WuiGlobalResourceList);
+
+ def _actionGlobalRsrcShowAdd(self):
+ """ Action wrapper. """
+ return self._actionGlobalRsrcShowAddEdit(WuiAdmin.ksActionGlobalRsrcAdd);
+
+ def _actionGlobalRsrcShowEdit(self):
+ """ Action wrapper. """
+ return self._actionGlobalRsrcShowAddEdit(WuiAdmin.ksActionGlobalRsrcEdit);
+
+ def _actionGlobalRsrcAdd(self):
+ """ Action wrapper. """
+ return self._actionGlobalRsrcAddEdit(WuiAdmin.ksActionGlobalRsrcAdd);
+
+ def _actionGlobalRsrcEdit(self):
+ """ Action wrapper. """
+ return self._actionGlobalRsrcAddEdit(WuiAdmin.ksActionGlobalRsrcEdit);
+
+ def _actionGlobalRsrcDel(self):
+ """ Action wrapper. """
+ from testmanager.core.globalresource import GlobalResourceData, GlobalResourceLogic;
+ return self._actionGenericDoDelOld(GlobalResourceLogic, GlobalResourceData.ksParam_idGlobalRsrc,
+ self.ksActionGlobalRsrcShowAll);
+
+ def _actionGlobalRsrcShowAddEdit(self, sAction): # pylint: disable=invalid-name
+ """Show Global Resource creation or edit dialog"""
+ from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData;
+ from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResource;
+
+ oGlobalResourceLogic = GlobalResourceLogic(self._oDb)
+ if sAction == WuiAdmin.ksActionGlobalRsrcEdit:
+ idGlobalRsrc = self.getIntParam(GlobalResourceData.ksParam_idGlobalRsrc, iDefault = -1)
+ oData = oGlobalResourceLogic.getById(idGlobalRsrc)
+ else:
+ oData = GlobalResourceData()
+ oData.convertToParamNull()
+
+ self._checkForUnknownParameters()
+
+ oContent = WuiGlobalResource(oData)
+ (self._sPageTitle, self._sPageBody) = oContent.showAddModifyPage(sAction)
+
+ return True
+
+ def _actionGlobalRsrcAddEdit(self, sAction):
+ """Add or modify Global Resource record"""
+ from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData;
+ from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResource;
+
+ oData = GlobalResourceData()
+ oData.initFromParams(self, fStrict=True)
+
+ self._checkForUnknownParameters()
+
+ if self._oSrvGlue.getMethod() != 'POST':
+ raise WuiException('Expected "POST" request, got "%s"' % (self._oSrvGlue.getMethod(),))
+
+ oGlobalResourceLogic = GlobalResourceLogic(self._oDb)
+ dErrors = oData.validateAndConvert(self._oDb);
+ if not dErrors:
+ if sAction == WuiAdmin.ksActionGlobalRsrcAdd:
+ oGlobalResourceLogic.addGlobalResource(self._oCurUser.uid, oData)
+ elif sAction == WuiAdmin.ksActionGlobalRsrcEdit:
+ idGlobalRsrc = self.getStringParam(GlobalResourceData.ksParam_idGlobalRsrc)
+ oGlobalResourceLogic.editGlobalResource(self._oCurUser.uid, idGlobalRsrc, oData)
+ else:
+ raise WuiException('Invalid parameter.')
+ self._sPageTitle = None;
+ self._sPageBody = None;
+ self._sRedirectTo = self._sActionUrlBase + self.ksActionGlobalRsrcShowAll;
+ else:
+ oContent = WuiGlobalResource(oData)
+ (self._sPageTitle, self._sPageBody) = oContent.showAddModifyPage(sAction, dErrors=dErrors)
+
+ return True
+
+
+ #
+ # Build Source actions
+ #
+
+ def _actionBuildSrcList(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceLogic;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrcList;
+ return self._actionGenericListing(BuildSourceLogic, WuiAdminBuildSrcList);
+
+ def _actionBuildSrcAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormAdd(BuildSourceData, WuiAdminBuildSrc);
+
+ def _actionBuildSrcAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormAddPost(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, self.ksActionBuildSrcList);
+
+ def _actionBuildSrcClone(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormClone( BuildSourceData, WuiAdminBuildSrc, 'idBuildSrc');
+
+ def _actionBuildSrcDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormDetails(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, 'idBuildSrc');
+
+ def _actionBuildSrcDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+ return self._actionGenericDoRemove(BuildSourceLogic, BuildSourceData.ksParam_idBuildSrc, self.ksActionBuildSrcList);
+
+ def _actionBuildSrcEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormEdit(BuildSourceData, WuiAdminBuildSrc, BuildSourceData.ksParam_idBuildSrc);
+
+ def _actionBuildSrcEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormEditPost(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, self.ksActionBuildSrcList);
+
+
+ #
+ # Build actions
+ #
+ def _actionBuildList(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildLogic;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuildList;
+ return self._actionGenericListing(BuildLogic, WuiAdminBuildList);
+
+ def _actionBuildAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormAdd(BuildData, WuiAdminBuild);
+
+ def _actionBuildAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData, BuildLogic;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormAddPost(BuildData, BuildLogic, WuiAdminBuild, self.ksActionBuildList);
+
+ def _actionBuildClone(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormClone( BuildData, WuiAdminBuild, 'idBuild');
+
+ def _actionBuildDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData, BuildLogic;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormDetails(BuildData, BuildLogic, WuiAdminBuild, 'idBuild');
+
+ def _actionBuildDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData, BuildLogic;
+ return self._actionGenericDoRemove(BuildLogic, BuildData.ksParam_idBuild, self.ksActionBuildList);
+
+ def _actionBuildEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormEdit(BuildData, WuiAdminBuild, BuildData.ksParam_idBuild);
+
+ def _actionBuildEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData, BuildLogic;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormEditPost(BuildData, BuildLogic, WuiAdminBuild, self.ksActionBuildList)
+
+
+ #
+ # Build Category actions
+ #
+ def _actionBuildCategoryList(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryLogic;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCatList;
+ return self._actionGenericListing(BuildCategoryLogic, WuiAdminBuildCatList);
+
+ def _actionBuildCategoryAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat;
+ return self._actionGenericFormAdd(BuildCategoryData, WuiAdminBuildCat);
+
+ def _actionBuildCategoryAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData, BuildCategoryLogic;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat;
+ return self._actionGenericFormAddPost(BuildCategoryData, BuildCategoryLogic, WuiAdminBuildCat,
+ self.ksActionBuildCategoryList);
+
+ def _actionBuildCategoryClone(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat;
+ return self._actionGenericFormClone(BuildCategoryData, WuiAdminBuildCat, 'idBuildCategory');
+
+ def _actionBuildCategoryDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData, BuildCategoryLogic;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat;
+ return self._actionGenericFormDetails(BuildCategoryData, BuildCategoryLogic, WuiAdminBuildCat, 'idBuildCategory');
+
+ def _actionBuildCategoryDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData, BuildCategoryLogic;
+ return self._actionGenericDoRemove(BuildCategoryLogic, BuildCategoryData.ksParam_idBuildCategory,
+ self.ksActionBuildCategoryList)
+
+
+ #
+ # Build Black List actions
+ #
+ def _actionBuildBlacklist(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistLogic;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminListOfBlacklistItems;
+ return self._actionGenericListing(BuildBlacklistLogic, WuiAdminListOfBlacklistItems);
+
+ def _actionBuildBlacklistAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormAdd(BuildBlacklistData, WuiAdminBuildBlacklist);
+
+ def _actionBuildBlacklistAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormAddPost(BuildBlacklistData, BuildBlacklistLogic,
+ WuiAdminBuildBlacklist, self.ksActionBuildBlacklist);
+
+ def _actionBuildBlacklistClone(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormClone(BuildBlacklistData, WuiAdminBuildBlacklist, 'idBlacklisting');
+
+ def _actionBuildBlacklistDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormDetails(BuildBlacklistData, BuildBlacklistLogic, WuiAdminBuildBlacklist, 'idBlacklisting');
+
+ def _actionBuildBlacklistDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic;
+ return self._actionGenericDoRemove(BuildBlacklistLogic, BuildBlacklistData.ksParam_idBlacklisting,
+ self.ksActionBuildBlacklist);
+
+ def _actionBuildBlacklistEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormEdit(BuildBlacklistData, WuiAdminBuildBlacklist, BuildBlacklistData.ksParam_idBlacklisting);
+
+ def _actionBuildBlacklistEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormEditPost(BuildBlacklistData, BuildBlacklistLogic, WuiAdminBuildBlacklist,
+ self.ksActionBuildBlacklist)
+
+
+ #
+ # Failure Category actions
+ #
+ def _actionFailureCategoryList(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryLogic;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategoryList;
+ return self._actionGenericListing(FailureCategoryLogic, WuiFailureCategoryList);
+
+ def _actionFailureCategoryAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormAdd(FailureCategoryData, WuiFailureCategory);
+
+ def _actionFailureCategoryAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormAddPost(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory,
+ self.ksActionFailureCategoryList)
+
+ def _actionFailureCategoryDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormDetails(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory);
+
+
+ def _actionFailureCategoryDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic;
+ return self._actionGenericDoRemove(FailureCategoryLogic, FailureCategoryData.ksParam_idFailureCategory,
+ self.ksActionFailureCategoryList);
+
+ def _actionFailureCategoryEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormEdit(FailureCategoryData, WuiFailureCategory,
+ FailureCategoryData.ksParam_idFailureCategory);
+
+ def _actionFailureCategoryEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormEditPost(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory,
+ self.ksActionFailureCategoryList);
+
+ #
+ # Failure Reason actions
+ #
+ def _actionFailureReasonList(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonLogic;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReasonList;
+ return self._actionGenericListing(FailureReasonLogic, WuiAdminFailureReasonList)
+
+ def _actionFailureReasonAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormAdd(FailureReasonData, WuiAdminFailureReason);
+
+ def _actionFailureReasonAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormAddPost(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason,
+ self.ksActionFailureReasonList);
+
+ def _actionFailureReasonDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormDetails(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason);
+
+ def _actionFailureReasonDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+ return self._actionGenericDoRemove(FailureReasonLogic, FailureReasonData.ksParam_idFailureReason,
+ self.ksActionFailureReasonList);
+
+ def _actionFailureReasonEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormEdit(FailureReasonData, WuiAdminFailureReason);
+
+
+ def _actionFailureReasonEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormEditPost(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason,
+ self.ksActionFailureReasonList)
+
+
+ #
+ # Overrides.
+ #
+
+ def _generatePage(self):
+ """Override parent handler in order to change page titte"""
+ if self._sPageTitle is not None:
+ self._sPageTitle = 'Test Manager Admin - ' + self._sPageTitle
+
+ return WuiDispatcherBase._generatePage(self)
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py
new file mode 100755
index 00000000..17944ec3
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminbuild.py $
+
+"""
+Test Manager WUI - Builds.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiBuildLogLink, \
+ WuiSvnLinkWithTooltip;
+from testmanager.core.build import BuildData, BuildCategoryLogic;
+from testmanager.core.buildblacklist import BuildBlacklistData;
+from testmanager.core.db import isDbTimestampInfinity;
+
+
+class WuiAdminBuild(WuiFormContentBase):
+ """
+ WUI Build HTML content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add Build'
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Modify Build - #%s' % (oData.idBuild,);
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Build - #%s' % (oData.idBuild,);
+ WuiFormContentBase.__init__(self, oData, sMode, 'Build', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO (BuildData.ksParam_idBuild, oData.idBuild, 'Build ID')
+ oForm.addTimestampRO(BuildData.ksParam_tsCreated, oData.tsCreated, 'Created')
+ oForm.addTimestampRO(BuildData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(BuildData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (BuildData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+
+ oForm.addComboBox (BuildData.ksParam_idBuildCategory, oData.idBuildCategory, 'Build category',
+ BuildCategoryLogic(self._oDisp.getDb()).fetchForCombo());
+
+ oForm.addInt (BuildData.ksParam_iRevision, oData.iRevision, 'Revision')
+ oForm.addText (BuildData.ksParam_sVersion, oData.sVersion, 'Version')
+ oForm.addWideText (BuildData.ksParam_sLogUrl, oData.sLogUrl, 'Log URL')
+ oForm.addWideText (BuildData.ksParam_sBinaries, oData.sBinaries, 'Binaries')
+ oForm.addCheckBox (BuildData.ksParam_fBinariesDeleted, oData.fBinariesDeleted, 'Binaries deleted')
+
+ oForm.addSubmit()
+ return True;
+
+
+class WuiAdminBuildList(WuiListContentBase):
+ """
+ WUI Admin Build List Content Generator.
+ """
+
+ ksResultsSortByOs_Darwin = ''
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Builds', sId = 'builds', fnDPrint = fnDPrint, oDisp = oDisp,
+ aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Product', 'Branch', 'Version',
+ 'Type', 'OS(es)', 'Author', 'Added',
+ 'Files', 'Action' ];
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ '', 'align="center"'];
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry];
+
+ aoActions = [];
+ if oEntry.sLogUrl is not None:
+ aoActions.append(WuiBuildLogLink(oEntry.sLogUrl, 'Build Log'));
+
+ dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistAdd,
+ BuildBlacklistData.ksParam_sProduct: oEntry.oCat.sProduct,
+ BuildBlacklistData.ksParam_sBranch: oEntry.oCat.sBranch,
+ BuildBlacklistData.ksParam_asTypes: oEntry.oCat.sType,
+ BuildBlacklistData.ksParam_asOsArches: oEntry.oCat.asOsArches,
+ BuildBlacklistData.ksParam_iFirstRevision: oEntry.iRevision,
+ BuildBlacklistData.ksParam_iLastRevision: oEntry.iRevision }
+
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Blacklist', WuiAdmin.ksScriptName, dParams),
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oEntry.idBuild,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }),
+ WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildClone,
+ BuildData.ksParam_idBuild: oEntry.idBuild,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }),
+ ];
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildEdit,
+ BuildData.ksParam_idBuild: oEntry.idBuild }),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDoRemove,
+ BuildData.ksParam_idBuild: oEntry.idBuild },
+ sConfirm = 'Are you sure you want to remove build #%d?' % (oEntry.idBuild,) ),
+ ];
+
+ return [ oEntry.idBuild,
+ oEntry.oCat.sProduct,
+ oEntry.oCat.sBranch,
+ WuiSvnLinkWithTooltip(oEntry.iRevision, oEntry.oCat.sRepository,
+ sName = '%s r%s' % (oEntry.sVersion, oEntry.iRevision,)),
+ oEntry.oCat.sType,
+ ' '.join(oEntry.oCat.asOsArches),
+ 'batch' if oEntry.uidAuthor is None else oEntry.uidAuthor,
+ self.formatTsShort(oEntry.tsCreated),
+ oEntry.sBinaries if not oEntry.fBinariesDeleted else '<Deleted>',
+ aoActions,
+ ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py
new file mode 100755
index 00000000..d265259b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminbuildblacklist.py $
+
+"""
+Test Manager WUI - Build Blacklist.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuibase import WuiException
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink
+from testmanager.core.buildblacklist import BuildBlacklistData
+from testmanager.core.failurereason import FailureReasonLogic
+from testmanager.core.db import TMDatabaseConnection
+from testmanager.core import coreconsts
+
+
+class WuiAdminBuildBlacklist(WuiFormContentBase):
+ """
+ WUI Build Black List Form.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ """
+ Prepare & initialize parent
+ """
+
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add Build Blacklist Entry'
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit Build Blacklist Entry'
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Build Black';
+ WuiFormContentBase.__init__(self, oData, sMode, 'BuildBlacklist', oDisp, sTitle);
+
+ #
+ # Additional data.
+ #
+ self.asTypes = coreconsts.g_kasBuildTypesAll
+ self.asOsArches = coreconsts.g_kasOsDotCpusAll
+
+ def _populateForm(self, oForm, oData):
+ """
+ Construct an HTML form
+ """
+
+ aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo()
+ if not aoFailureReasons:
+ from testmanager.webui.wuiadmin import WuiAdmin
+ raise WuiException('Please <a href="%s?%s=%s">add</a> some Failure Reasons first.'
+ % (WuiAdmin.ksScriptName, WuiAdmin.ksParamAction, WuiAdmin.ksActionFailureReasonAdd));
+
+ asTypes = self.getListOfItems(self.asTypes, oData.asTypes)
+ asOsArches = self.getListOfItems(self.asOsArches, oData.asOsArches)
+
+ oForm.addIntRO (BuildBlacklistData.ksParam_idBlacklisting, oData.idBlacklisting, 'Blacklist item ID')
+ oForm.addTimestampRO(BuildBlacklistData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(BuildBlacklistData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (BuildBlacklistData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+
+ oForm.addComboBox (BuildBlacklistData.ksParam_idFailureReason, oData.idFailureReason, 'Failure Reason',
+ aoFailureReasons)
+
+ oForm.addText (BuildBlacklistData.ksParam_sProduct, oData.sProduct, 'Product')
+ oForm.addText (BuildBlacklistData.ksParam_sBranch, oData.sBranch, 'Branch')
+ oForm.addListOfTypes(BuildBlacklistData.ksParam_asTypes, asTypes, 'Build types')
+ oForm.addListOfOsArches(BuildBlacklistData.ksParam_asOsArches, asOsArches, 'Target architectures')
+ oForm.addInt (BuildBlacklistData.ksParam_iFirstRevision, oData.iFirstRevision, 'First revision')
+ oForm.addInt (BuildBlacklistData.ksParam_iLastRevision, oData.iLastRevision, 'Last revision (incl)')
+
+ oForm.addSubmit();
+
+ return True;
+
+
+class WuiAdminListOfBlacklistItems(WuiListContentBase):
+ """
+ WUI Admin Build Blacklist Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Build Blacklist', sId = 'buildsBlacklist',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Failure Reason',
+ 'Product', 'Branch', 'Type',
+ 'OS(es)', 'First Revision', 'Last Revision',
+ 'Actions' ]
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"' ]
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry]
+
+ sShortFailReason = FailureReasonLogic(TMDatabaseConnection()).getById(oEntry.idFailureReason).sShort
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistDetails,
+ BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting }),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Edit', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistEdit,
+ BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting }),
+ WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistClone,
+ BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting,
+ WuiAdmin.ksParamEffectiveDate: oEntry.tsEffective, }),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistDoRemove,
+ BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting },
+ sConfirm = 'Are you sure you want to remove black list entry #%d?' % (oEntry.idBlacklisting,)),
+ ];
+
+ return [ oEntry.idBlacklisting,
+ sShortFailReason,
+ oEntry.sProduct,
+ oEntry.sBranch,
+ oEntry.asTypes,
+ oEntry.asOsArches,
+ oEntry.iFirstRevision,
+ oEntry.iLastRevision,
+ aoActions
+ ];
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py
new file mode 100755
index 00000000..87d76fba
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminbuildcategory.py $
+
+"""
+Test Manager WUI - Build categories.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from common import webutils;
+from testmanager.webui.wuicontentbase import WuiListContentBase, WuiFormContentBase, WuiRawHtml, WuiTmLink;
+from testmanager.core.build import BuildCategoryData
+from testmanager.core import coreconsts;
+
+
+class WuiAdminBuildCatList(WuiListContentBase):
+ """
+ WUI Build Category List Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Build Categories', sId = 'buildcategories',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = ([ 'ID', 'Product', 'Repository', 'Branch', 'Build Type', 'OS/Architectures', 'Actions' ]);
+ self._asColumnAttribs = (['align="right"', '', '', '', '', 'align="center"' ]);
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ oEntry = self._aoEntries[iEntry];
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryDetails,
+ BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryClone,
+ BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }),
+ WuiTmLink('Try Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryDoRemove,
+ BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }),
+ ];
+
+ sHtml = '<ul class="tmshowall">\n';
+ for sOsArch in oEntry.asOsArches:
+ sHtml += ' <li class="tmshowall">%s</li>\n' % (webutils.escapeElem(sOsArch),);
+ sHtml += '</ul>\n'
+
+ return [ oEntry.idBuildCategory,
+ oEntry.sRepository,
+ oEntry.sProduct,
+ oEntry.sBranch,
+ oEntry.sType,
+ WuiRawHtml(sHtml),
+ aoActions,
+ ];
+
+
+class WuiAdminBuildCat(WuiFormContentBase):
+ """
+ WUI Build Category Form Content Generator.
+ """
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Create Build Category';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ assert False, 'not possible'
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Build Category- %s' % (oData.idBuildCategory,);
+ WuiFormContentBase.__init__(self, oData, sMode, 'BuildCategory', oDisp, sTitle, fEditable = False);
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO( BuildCategoryData.ksParam_idBuildCategory, oData.idBuildCategory, 'Build Category ID')
+ oForm.addText( BuildCategoryData.ksParam_sRepository, oData.sRepository, 'VCS repository name');
+ oForm.addText( BuildCategoryData.ksParam_sProduct, oData.sProduct, 'Product name')
+ oForm.addText( BuildCategoryData.ksParam_sBranch, oData.sBranch, 'Branch name')
+ oForm.addText( BuildCategoryData.ksParam_sType, oData.sType, 'Build type')
+
+ aoOsArches = [[sOsArch, sOsArch in oData.asOsArches, sOsArch] for sOsArch in coreconsts.g_kasOsDotCpusAll];
+ oForm.addListOfOsArches(BuildCategoryData.ksParam_asOsArches, aoOsArches, 'Target architectures');
+
+ if self._sMode != WuiFormContentBase.ksMode_Show:
+ oForm.addSubmit();
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py
new file mode 100755
index 00000000..52d05f07
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminbuildsource.py $
+
+"""
+Test Manager WUI - Build Sources.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml;
+from testmanager.core import coreconsts;
+from testmanager.core.db import isDbTimestampInfinity;
+from testmanager.core.buildsource import BuildSourceData;
+
+
+class WuiAdminBuildSrc(WuiFormContentBase):
+ """
+ WUI Build Sources HTML content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ assert isinstance(oData, BuildSourceData);
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'New Build Source';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit Build Source - %s (#%s)' % (oData.sName, oData.idBuildSrc,);
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Build Source - %s (#%s)' % (oData.sName, oData.idBuildSrc,);
+ WuiFormContentBase.__init__(self, oData, sMode, 'BuildSrc', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO (BuildSourceData.ksParam_idBuildSrc, oData.idBuildSrc, 'Build Source item ID')
+ oForm.addTimestampRO(BuildSourceData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(BuildSourceData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (BuildSourceData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+ oForm.addText (BuildSourceData.ksParam_sName, oData.sName, 'Name')
+ oForm.addText (BuildSourceData.ksParam_sDescription, oData.sDescription, 'Description')
+ oForm.addText (BuildSourceData.ksParam_sProduct, oData.sProduct, 'Product')
+ oForm.addText (BuildSourceData.ksParam_sBranch, oData.sBranch, 'Branch')
+ asTypes = self.getListOfItems(coreconsts.g_kasBuildTypesAll, oData.asTypes);
+ oForm.addListOfTypes(BuildSourceData.ksParam_asTypes, asTypes, 'Build types')
+ asOsArches = self.getListOfItems(coreconsts.g_kasOsDotCpusAll, oData.asOsArches);
+ oForm.addListOfOsArches(BuildSourceData.ksParam_asOsArches, asOsArches, 'Target architectures')
+ oForm.addInt (BuildSourceData.ksParam_iFirstRevision, oData.iFirstRevision, 'Starting from revision')
+ oForm.addInt (BuildSourceData.ksParam_iLastRevision, oData.iLastRevision, 'Ending by revision')
+ oForm.addLong (BuildSourceData.ksParam_cSecMaxAge,
+ utils.formatIntervalSeconds2(oData.cSecMaxAge) if oData.cSecMaxAge not in [-1, '', None] else '',
+ 'Max age in seconds');
+ oForm.addSubmit();
+ return True;
+
+class WuiAdminBuildSrcList(WuiListContentBase):
+ """
+ WUI Build Source content generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Registered Build Sources', sId = 'build sources',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = ['ID', 'Name', 'Description', 'Product',
+ 'Branch', 'Build Types', 'OS/ARCH', 'First Revision', 'Last Revision', 'Max Age',
+ 'Actions' ];
+ self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="left"', 'align="left"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"' ];
+
+ def _getSubList(self, aList):
+ """
+ Convert pythonic list into HTML list
+ """
+ if aList not in (None, []):
+ sHtml = ' <ul class="tmshowall">\n'
+ for sTmp in aList:
+ sHtml += ' <li class="tmshowall">%s</a></li>\n' % (webutils.escapeElem(sTmp),);
+ sHtml += ' </ul>\n';
+ else:
+ sHtml = '<ul class="tmshowall"><li class="tmshowall">Any</li></ul>\n';
+
+ return WuiRawHtml(sHtml);
+
+ def _formatListEntry(self, iEntry):
+ """
+ Format *show all* table entry
+ """
+
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry]
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcClone,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }),
+ ];
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcEdit,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc } ),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDoRemove,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc },
+ sConfirm = 'Are you sure you want to remove build source #%d?' % (oEntry.idBuildSrc,) )
+ ];
+
+ return [ oEntry.idBuildSrc,
+ oEntry.sName,
+ oEntry.sDescription,
+ oEntry.sProduct,
+ oEntry.sBranch,
+ self._getSubList(oEntry.asTypes),
+ self._getSubList(oEntry.asOsArches),
+ oEntry.iFirstRevision,
+ oEntry.iLastRevision,
+ utils.formatIntervalSeconds2(oEntry.cSecMaxAge) if oEntry.cSecMaxAge is not None else None,
+ aoActions,
+ ]
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py
new file mode 100755
index 00000000..d02931de
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminfailurecategory.py $
+
+"""
+Test Manager WUI - Failure Categories Web content generator.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiContentBase, WuiListContentBase, WuiTmLink;
+from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReasonList;
+from testmanager.core.failurecategory import FailureCategoryData;
+from testmanager.core.failurereason import FailureReasonLogic;
+
+
+class WuiFailureReasonCategoryLink(WuiTmLink):
+ """ Link to a failure category. """
+ def __init__(self, idFailureCategory, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
+ if fBracketed is None:
+ fBracketed = len(sName) > 2;
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ WuiTmLink.__init__(self, sName = sName,
+ sUrlBase = WuiAdmin.ksScriptName,
+ dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDetails,
+ FailureCategoryData.ksParam_idFailureCategory: idFailureCategory, },
+ fBracketed = fBracketed,
+ sTitle = sTitle);
+ self.idFailureCategory = idFailureCategory;
+
+
+
+class WuiFailureCategory(WuiFormContentBase):
+ """
+ WUI Failure Category HTML content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ """
+ Prepare & initialize parent
+ """
+
+ sTitle = 'Failure Category';
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add ' + sTitle;
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit ' + sTitle;
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+
+ WuiFormContentBase.__init__(self, oData, sMode, 'FailureCategory', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ """
+ Construct an HTML form
+ """
+
+ oForm.addIntRO (FailureCategoryData.ksParam_idFailureCategory, oData.idFailureCategory, 'Failure Category ID')
+ oForm.addTimestampRO(FailureCategoryData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(FailureCategoryData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (FailureCategoryData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+ oForm.addText (FailureCategoryData.ksParam_sShort, oData.sShort, 'Short Description')
+ oForm.addText (FailureCategoryData.ksParam_sFull, oData.sFull, 'Full Description')
+
+ oForm.addSubmit()
+
+ return True;
+
+ def _generatePostFormContent(self, oData):
+ """
+ Adds a table with the category members below the form.
+ """
+ if oData.idFailureCategory is not None and oData.idFailureCategory >= 0:
+ oLogic = FailureReasonLogic(self._oDisp.getDb());
+ tsNow = self._oDisp.getNow();
+ cMax = 4096;
+ aoEntries = oLogic.fetchForListingInCategory(0, cMax, tsNow, oData.idFailureCategory)
+ if aoEntries:
+ oList = WuiAdminFailureReasonList(aoEntries, 0, cMax, tsNow, fnDPrint = None, oDisp = self._oDisp);
+ return [ [ 'Members', oList.show(fShowNavigation = False)[1]], ];
+ return [];
+
+
+
+class WuiFailureCategoryList(WuiListContentBase):
+ """
+ WUI Admin Failure Category Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Failure Categories', sId = 'failureCategories',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Short Description', 'Full Description', 'Actions' ]
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"']
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry]
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDetails,
+ FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory }),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryEdit,
+ FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory }),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDoRemove,
+ FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory },
+ sConfirm = 'Do you really want to remove failure cateogry #%d?' % (oEntry.idFailureCategory,)),
+ ]
+
+ return [ oEntry.idFailureCategory,
+ oEntry.sShort,
+ oEntry.sFull,
+ aoActions,
+ ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py
new file mode 100755
index 00000000..5fa76227
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminfailurereason.py $
+
+"""
+Test Manager WUI - Failure Reasons Web content generator.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuibase import WuiException
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiContentBase, WuiTmLink;
+from testmanager.core.failurereason import FailureReasonData;
+from testmanager.core.failurecategory import FailureCategoryLogic;
+from testmanager.core.db import TMDatabaseConnection;
+
+
+
+class WuiFailureReasonDetailsLink(WuiTmLink):
+ """ Short link to a failure reason. """
+ def __init__(self, idFailureReason, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
+ if fBracketed is None:
+ fBracketed = len(sName) > 2;
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ WuiTmLink.__init__(self, sName = sName,
+ sUrlBase = WuiAdmin.ksScriptName,
+ dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
+ FailureReasonData.ksParam_idFailureReason: idFailureReason, },
+ fBracketed = fBracketed);
+ self.idFailureReason = idFailureReason;
+
+
+
+class WuiFailureReasonAddLink(WuiTmLink):
+ """ Link for adding a failure reason. """
+ def __init__(self, sName = WuiContentBase.ksShortAddLink, sTitle = None, fBracketed = None):
+ if fBracketed is None:
+ fBracketed = len(sName) > 2;
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ WuiTmLink.__init__(self, sName = sName,
+ sUrlBase = WuiAdmin.ksScriptName,
+ dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonAdd, },
+ fBracketed = fBracketed);
+
+
+
+class WuiAdminFailureReason(WuiFormContentBase):
+ """
+ WUI Failure Reason HTML content generator.
+ """
+
+ def __init__(self, oFailureReasonData, sMode, oDisp):
+ """
+ Prepare & initialize parent
+ """
+
+ sTitle = 'Failure Reason';
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add' + sTitle;
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit' + sTitle;
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+
+ WuiFormContentBase.__init__(self, oFailureReasonData, sMode, 'FailureReason', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ """
+ Construct an HTML form
+ """
+
+ aoFailureCategories = FailureCategoryLogic(TMDatabaseConnection()).getFailureCategoriesForCombo()
+ if not aoFailureCategories:
+ from testmanager.webui.wuiadmin import WuiAdmin
+ sExceptionMsg = 'Please <a href="%s?%s=%s">add</a> Failure Category first.' % \
+ (WuiAdmin.ksScriptName, WuiAdmin.ksParamAction, WuiAdmin.ksActionFailureCategoryAdd)
+
+ raise WuiException(sExceptionMsg)
+
+ oForm.addIntRO (FailureReasonData.ksParam_idFailureReason, oData.idFailureReason, 'Failure Reason ID')
+ oForm.addTimestampRO (FailureReasonData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO (FailureReasonData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (FailureReasonData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+
+ oForm.addComboBox (FailureReasonData.ksParam_idFailureCategory, oData.idFailureCategory, 'Failure Category',
+ aoFailureCategories)
+
+ oForm.addText (FailureReasonData.ksParam_sShort, oData.sShort, 'Short Description')
+ oForm.addText (FailureReasonData.ksParam_sFull, oData.sFull, 'Full Description')
+ oForm.addInt (FailureReasonData.ksParam_iTicket, oData.iTicket, 'Ticket Number')
+ oForm.addMultilineText(FailureReasonData.ksParam_asUrls, oData.asUrls, 'Other URLs to reports '
+ 'or discussions of the '
+ 'observed symptoms')
+ oForm.addSubmit()
+
+ return True
+
+
+class WuiAdminFailureReasonList(WuiListContentBase):
+ """
+ WUI Admin Failure Reasons Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Failure Reasons', sId = 'failureReasons',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Category', 'Short Description',
+ 'Full Description', 'Ticket', 'External References', 'Actions' ]
+
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"',
+ 'align="center"',' align="center"', 'align="center"', 'align="center"']
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureReasonCategoryLink;
+ oEntry = self._aoEntries[iEntry]
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
+ FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason } ),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonEdit,
+ FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason } ),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDoRemove,
+ FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason },
+ sConfirm = 'Are you sure you want to remove failure reason #%d?' % (oEntry.idFailureReason,)),
+ ];
+
+ return [ oEntry.idFailureReason,
+ WuiFailureReasonCategoryLink(oEntry.idFailureCategory, sName = oEntry.oCategory.sShort, fBracketed = False),
+ oEntry.sShort,
+ oEntry.sFull,
+ oEntry.iTicket,
+ oEntry.asUrls,
+ aoActions,
+ ]
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py
new file mode 100755
index 00000000..b748ea2f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminglobalrsrc.py $
+
+"""
+Test Manager WUI - Global resources.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from testmanager.webui.wuibase import WuiException
+from testmanager.webui.wuicontentbase import WuiContentBase
+from testmanager.webui.wuihlpform import WuiHlpForm
+from testmanager.core.globalresource import GlobalResourceData
+from testmanager.webui.wuicontentbase import WuiListContentBase, WuiTmLink
+
+
+class WuiGlobalResource(WuiContentBase):
+ """
+ WUI global resources content generator.
+ """
+
+ def __init__(self, oData, fnDPrint = None):
+ """
+ Do necessary initializations
+ """
+ WuiContentBase.__init__(self, fnDPrint)
+ self._oData = oData
+
+ def showAddModifyPage(self, sAction, dErrors = None):
+ """
+ Render add global resource HTML form.
+ """
+ from testmanager.webui.wuiadmin import WuiAdmin
+
+ sFormActionUrl = '%s?%s=%s' % (WuiAdmin.ksScriptName,
+ WuiAdmin.ksParamAction, sAction)
+ if sAction == WuiAdmin.ksActionGlobalRsrcAdd:
+ sTitle = 'Add Global Resource'
+ elif sAction == WuiAdmin.ksActionGlobalRsrcEdit:
+ sTitle = 'Modify Global Resource'
+ sFormActionUrl += '&%s=%s' % (GlobalResourceData.ksParam_idGlobalRsrc, self._oData.idGlobalRsrc)
+ else:
+ raise WuiException('Invalid paraemter "%s"' % (sAction,))
+
+ oForm = WuiHlpForm('globalresourceform',
+ sFormActionUrl,
+ dErrors if dErrors is not None else {})
+
+ if sAction == WuiAdmin.ksActionGlobalRsrcAdd:
+ oForm.addIntRO (GlobalResourceData.ksParam_idGlobalRsrc, self._oData.idGlobalRsrc, 'Global Resource ID')
+ oForm.addTimestampRO(GlobalResourceData.ksParam_tsEffective, self._oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(GlobalResourceData.ksParam_tsExpire, self._oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (GlobalResourceData.ksParam_uidAuthor, self._oData.uidAuthor, 'Changed by UID')
+ oForm.addText (GlobalResourceData.ksParam_sName, self._oData.sName, 'Name')
+ oForm.addText (GlobalResourceData.ksParam_sDescription, self._oData.sDescription, 'Description')
+ oForm.addCheckBox (GlobalResourceData.ksParam_fEnabled, self._oData.fEnabled, 'Enabled')
+
+ oForm.addSubmit('Submit')
+
+ return (sTitle, oForm.finalize())
+
+
+class WuiGlobalResourceList(WuiListContentBase):
+ """
+ WUI Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Global Resources', sId = 'globalResources',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Name', 'Description', 'Enabled', 'Actions' ]
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"']
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry]
+
+ aoActions = [ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionGlobalRsrcShowEdit,
+ GlobalResourceData.ksParam_idGlobalRsrc: oEntry.idGlobalRsrc }),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionGlobalRsrcDel,
+ GlobalResourceData.ksParam_idGlobalRsrc: oEntry.idGlobalRsrc },
+ sConfirm = 'Are you sure you want to remove global resource #%d?' % (oEntry.idGlobalRsrc,)),
+ ];
+
+ return [ oEntry.idGlobalRsrc,
+ oEntry.sName,
+ oEntry.sDescription,
+ oEntry.fEnabled,
+ aoActions, ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py
new file mode 100755
index 00000000..e7a43717
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py
@@ -0,0 +1,205 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminschedgroup.py $
+
+"""
+Test Manager WUI - Scheduling groups.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+from testmanager.core.db import isDbTimestampInfinity;
+from testmanager.core.schedgroup import SchedGroupData, SchedGroupDataEx;
+from testmanager.core.testgroup import TestGroupData, TestGroupLogic;
+from testmanager.core.testbox import TestBoxLogic;
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml;
+from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLink;
+
+
+class WuiSchedGroup(WuiFormContentBase):
+ """
+ WUI Scheduling Groups HTML content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ assert isinstance(oData, SchedGroupData);
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'New Scheduling Group';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit Scheduling Group'
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Scheduling Group';
+ WuiFormContentBase.__init__(self, oData, sMode, 'SchedGroup', oDisp, sTitle);
+
+ # Read additional bits form the DB, unless we're in
+ if sMode != WuiFormContentBase.ksMode_Show:
+ self._aoAllRelevantTestGroups = TestGroupLogic(oDisp.getDb()).getAll();
+ self._aoAllRelevantTestBoxes = TestBoxLogic(oDisp.getDb()).getAll();
+ else:
+ self._aoAllRelevantTestGroups = [oMember.oTestGroup for oMember in oData.aoMembers];
+ self._aoAllRelevantTestBoxes = [oMember.oTestBox for oMember in oData.aoTestBoxes];
+
+ def _populateForm(self, oForm, oData): # type: (WuiHlpForm, SchedGroupDataEx) -> bool
+ """
+ Construct an HTML form
+ """
+
+ oForm.addIntRO( SchedGroupData.ksParam_idSchedGroup, oData.idSchedGroup, 'ID')
+ oForm.addTimestampRO(SchedGroupData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(SchedGroupData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO( SchedGroupData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+ oForm.addText( SchedGroupData.ksParam_sName, oData.sName, 'Name')
+ oForm.addText( SchedGroupData.ksParam_sDescription, oData.sDescription, 'Description')
+ oForm.addCheckBox( SchedGroupData.ksParam_fEnabled, oData.fEnabled, 'Enabled')
+
+ oForm.addComboBox( SchedGroupData.ksParam_enmScheduler, oData.enmScheduler, 'Scheduler type',
+ SchedGroupData.kasSchedulerDesc)
+
+ aoBuildSrcIds = BuildSourceLogic(self._oDisp.getDb()).fetchForCombo();
+ oForm.addComboBox( SchedGroupData.ksParam_idBuildSrc, oData.idBuildSrc, 'Build source', aoBuildSrcIds);
+ oForm.addComboBox( SchedGroupData.ksParam_idBuildSrcTestSuite,
+ oData.idBuildSrcTestSuite, 'Test suite', aoBuildSrcIds);
+
+ oForm.addListOfSchedGroupMembers(SchedGroupDataEx.ksParam_aoMembers,
+ oData.aoMembers, self._aoAllRelevantTestGroups, 'Test groups',
+ oData.idSchedGroup, fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show);
+
+ oForm.addListOfSchedGroupBoxes(SchedGroupDataEx.ksParam_aoTestBoxes,
+ oData.aoTestBoxes, self._aoAllRelevantTestBoxes, 'Test boxes',
+ oData.idSchedGroup, fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show);
+
+ oForm.addMultilineText(SchedGroupData.ksParam_sComment, oData.sComment, 'Comment');
+ oForm.addSubmit()
+
+ return True;
+
+class WuiAdminSchedGroupList(WuiListContentBase):
+ """
+ Content generator for the schedule group listing.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Registered Scheduling Groups', sId = 'schedgroups',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = [
+ 'ID', 'Name', 'Enabled', 'Scheduler Type',
+ 'Build Source', 'Validation Kit Source', 'Test Groups', 'TestBoxes', 'Note', 'Actions',
+ ];
+
+ self._asColumnAttribs = [
+ 'align="right"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', '', '', 'align="center"', 'align="center"',
+ ];
+
+ def _formatListEntry(self, iEntry):
+ """
+ Format *show all* table entry
+ """
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry] # type: SchedGroupDataEx
+
+ oBuildSrc = None;
+ if oEntry.idBuildSrc is not None:
+ oBuildSrc = WuiTmLink(oEntry.oBuildSrc.sName if oEntry.oBuildSrc else str(oEntry.idBuildSrc),
+ WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc, });
+
+ oValidationKitSrc = None;
+ if oEntry.idBuildSrcTestSuite is not None:
+ oValidationKitSrc = WuiTmLink(oEntry.oBuildSrcValidationKit.sName if oEntry.oBuildSrcValidationKit
+ else str(oEntry.idBuildSrcTestSuite),
+ WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrcTestSuite, });
+
+ # Test groups
+ aoMembers = [];
+ for oMember in oEntry.aoMembers:
+ aoMembers.append(WuiTmLink(oMember.oTestGroup.sName, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDetails,
+ TestGroupData.ksParam_idTestGroup: oMember.idTestGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, },
+ sTitle = '#%s' % (oMember.idTestGroup,) if oMember.oTestGroup.sDescription is None
+ else '#%s - %s' % (oMember.idTestGroup, oMember.oTestGroup.sDescription,) ));
+
+ # Test boxes.
+ aoTestBoxes = [];
+ for oRelation in oEntry.aoTestBoxes:
+ oTestBox = oRelation.oTestBox;
+ if oTestBox:
+ aoTestBoxes.append(WuiTestBoxDetailsLink(oTestBox, fBracketed = True, tsNow = self._tsEffectiveDate));
+ else:
+ aoTestBoxes.append(WuiRawHtml('#%s' % (oRelation.idTestBox,)));
+
+ # Actions
+ aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupDetails,
+ SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } ),];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupEdit,
+ SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup } ));
+ aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupClone,
+ SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } ));
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupDoRemove,
+ SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup },
+ sConfirm = 'Are you sure you want to remove scheduling group #%d?'
+ % (oEntry.idSchedGroup,)));
+
+ return [
+ oEntry.idSchedGroup,
+ oEntry.sName,
+ oEntry.fEnabled,
+ oEntry.enmScheduler,
+ oBuildSrc,
+ oValidationKitSrc,
+ aoMembers,
+ aoTestBoxes,
+ self._formatCommentCell(oEntry.sComment),
+ aoActions,
+ ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py
new file mode 100755
index 00000000..88a3475a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# "$Id: wuiadminschedqueue.py $"
+
+"""
+Test Manager WUI - Admin - Scheduling Queue.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports
+from testmanager.webui.wuicontentbase import WuiListContentBase
+
+
+class WuiAdminSchedQueueList(WuiListContentBase):
+ """
+ WUI Scheduling Queue Content Generator.
+ """
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ tsEffective = None; # Not relevant, no history on the scheduling queue.
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'Scheduling Queue',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns,
+ fTimeNavigation = False);
+ self._asColumnHeaders = [
+ 'Last Run', 'Scheduling Group', 'Test Group', 'Test Case', 'Config State', 'Item ID'
+ ];
+ self._asColumnAttribs = [
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"'
+ ];
+ self._iPrevPerSchedGroupRowNumber = 0;
+
+ def _formatListEntry(self, iEntry):
+ oEntry = self._aoEntries[iEntry] # type: SchedQueueEntry
+ sState = 'up-to-date' if oEntry.fUpToDate else 'outdated';
+ return [ oEntry.tsLastScheduled, oEntry.sSchedGroup, oEntry.sTestGroup, oEntry.sTestCase, sState, oEntry.idItem ];
+
+ def _formatListEntryHtml(self, iEntry):
+ sHtml = WuiListContentBase._formatListEntryHtml(self, iEntry);
+
+ # Insert separator row?
+ if iEntry < len(self._aoEntries):
+ oEntry = self._aoEntries[iEntry] # type: SchedQueueEntry
+ if oEntry.iPerSchedGroupRowNumber != self._iPrevPerSchedGroupRowNumber:
+ if iEntry > 0 and iEntry + 1 < min(len(self._aoEntries), self._cItemsPerPage):
+ sHtml += '<tr class="tmseparator"><td colspan=%s> </td></tr>\n' % (len(self._asColumnHeaders),);
+ self._iPrevPerSchedGroupRowNumber = oEntry.iPerSchedGroupRowNumber;
+ return sHtml;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py
new file mode 100755
index 00000000..94057524
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py
@@ -0,0 +1,447 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminsystemchangelog.py $
+
+"""
+Test Manager WUI - Admin - System changelog.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+from common import webutils;
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiListContentBase, WuiHtmlKeeper, WuiAdminLink, \
+ WuiMainLink, WuiElementText, WuiHtmlBase;
+
+from testmanager.core.base import AttributeChangeEntryPre;
+from testmanager.core.buildblacklist import BuildBlacklistLogic, BuildBlacklistData;
+from testmanager.core.build import BuildLogic, BuildData;
+from testmanager.core.buildsource import BuildSourceLogic, BuildSourceData;
+from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData;
+from testmanager.core.failurecategory import FailureCategoryLogic, FailureCategoryData;
+from testmanager.core.failurereason import FailureReasonLogic, FailureReasonData;
+from testmanager.core.systemlog import SystemLogData;
+from testmanager.core.systemchangelog import SystemChangelogLogic;
+from testmanager.core.schedgroup import SchedGroupLogic, SchedGroupData;
+from testmanager.core.testbox import TestBoxLogic, TestBoxData;
+from testmanager.core.testcase import TestCaseLogic, TestCaseData;
+from testmanager.core.testgroup import TestGroupLogic, TestGroupData;
+from testmanager.core.testset import TestSetData;
+from testmanager.core.useraccount import UserAccountLogic, UserAccountData;
+
+
+class WuiAdminSystemChangelogList(WuiListContentBase):
+ """
+ WUI System Changelog Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, cDaysBack, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'System Changelog',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = [ 'When', 'User', 'Event', 'Details' ];
+ self._asColumnAttribs = [ 'align="center"', 'align="center"', '', '' ];
+ self._oBuildBlacklistLogic = BuildBlacklistLogic(oDisp.getDb());
+ self._oBuildLogic = BuildLogic(oDisp.getDb());
+ self._oBuildSourceLogic = BuildSourceLogic(oDisp.getDb());
+ self._oFailureCategoryLogic = FailureCategoryLogic(oDisp.getDb());
+ self._oFailureReasonLogic = FailureReasonLogic(oDisp.getDb());
+ self._oGlobalResourceLogic = GlobalResourceLogic(oDisp.getDb());
+ self._oSchedGroupLogic = SchedGroupLogic(oDisp.getDb());
+ self._oTestBoxLogic = TestBoxLogic(oDisp.getDb());
+ self._oTestCaseLogic = TestCaseLogic(oDisp.getDb());
+ self._oTestGroupLogic = TestGroupLogic(oDisp.getDb());
+ self._oUserAccountLogic = UserAccountLogic(oDisp.getDb());
+ self._sPrevDate = '';
+ _ = cDaysBack;
+
+ # oDetails = self._createBlacklistingDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ def _createBlacklistingDetailsLink(self, idBlacklisting, tsEffective):
+ """ Creates a link to the build source details. """
+ oBlacklisting = self._oBuildBlacklistLogic.cachedLookup(idBlacklisting);
+ if oBlacklisting is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink('Blacklisting #%u' % (oBlacklisting.idBlacklisting,),
+ WuiAdmin.ksActionBuildBlacklistDetails, tsEffective,
+ { BuildBlacklistData.ksParam_idBlacklisting: oBlacklisting.idBlacklisting },
+ fBracketed = False);
+ return WuiElementText('[blacklisting #%u not found]' % (idBlacklisting,));
+
+ def _createBuildDetailsLink(self, idBuild, tsEffective):
+ """ Creates a link to the build details. """
+ oBuild = self._oBuildLogic.cachedLookup(idBuild);
+ if oBuild is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink('%s %sr%u' % ( oBuild.oCat.sProduct, oBuild.sVersion, oBuild.iRevision),
+ WuiAdmin.ksActionBuildDetails, tsEffective,
+ { BuildData.ksParam_idBuild: oBuild.idBuild },
+ fBracketed = False,
+ sTitle = 'build #%u for %s, type %s'
+ % (oBuild.idBuild, ' & '.join(oBuild.oCat.asOsArches), oBuild.oCat.sType));
+ return WuiElementText('[build #%u not found]' % (idBuild,));
+
+ def _createBuildSourceDetailsLink(self, idBuildSrc, tsEffective):
+ """ Creates a link to the build source details. """
+ oBuildSource = self._oBuildSourceLogic.cachedLookup(idBuildSrc);
+ if oBuildSource is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oBuildSource.sName, WuiAdmin.ksActionBuildSrcDetails, tsEffective,
+ { BuildSourceData.ksParam_idBuildSrc: oBuildSource.idBuildSrc },
+ fBracketed = False,
+ sTitle = 'Build source #%u' % (oBuildSource.idBuildSrc,));
+ return WuiElementText('[build source #%u not found]' % (idBuildSrc,));
+
+ def _createFailureCategoryDetailsLink(self, idFailureCategory, tsEffective):
+ """ Creates a link to the failure category details. """
+ oFailureCategory = self._oFailureCategoryLogic.cachedLookup(idFailureCategory);
+ if oFailureCategory is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oFailureCategory.sShort, WuiAdmin.ksActionFailureCategoryDetails, tsEffective,
+ { FailureCategoryData.ksParam_idFailureCategory: oFailureCategory.idFailureCategory },
+ fBracketed = False,
+ sTitle = 'Failure category #%u' % (oFailureCategory.idFailureCategory,));
+ return WuiElementText('[failure category #%u not found]' % (idFailureCategory,));
+
+ def _createFailureReasonDetailsLink(self, idFailureReason, tsEffective):
+ """ Creates a link to the failure reason details. """
+ oFailureReason = self._oFailureReasonLogic.cachedLookup(idFailureReason);
+ if oFailureReason is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oFailureReason.sShort, WuiAdmin.ksActionFailureReasonDetails, tsEffective,
+ { FailureReasonData.ksParam_idFailureReason: oFailureReason.idFailureReason },
+ fBracketed = False,
+ sTitle = 'Failure reason #%u, category %s'
+ % (oFailureReason.idFailureReason, oFailureReason.oCategory.sShort));
+ return WuiElementText('[failure reason #%u not found]' % (idFailureReason,));
+
+ def _createGlobalResourceDetailsLink(self, idGlobalRsrc, tsEffective):
+ """ Creates a link to the global resource details. """
+ oGlobalResource = self._oGlobalResourceLogic.cachedLookup(idGlobalRsrc);
+ if oGlobalResource is not None:
+ return WuiAdminLink(oGlobalResource.sName, '@todo', tsEffective,
+ { GlobalResourceData.ksParam_idGlobalRsrc: oGlobalResource.idGlobalRsrc },
+ fBracketed = False,
+ sTitle = 'Global resource #%u' % (oGlobalResource.idGlobalRsrc,));
+ return WuiElementText('[global resource #%u not found]' % (idGlobalRsrc,));
+
+ def _createSchedGroupDetailsLink(self, idSchedGroup, tsEffective):
+ """ Creates a link to the scheduling group details. """
+ oSchedGroup = self._oSchedGroupLogic.cachedLookup(idSchedGroup);
+ if oSchedGroup is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oSchedGroup.sName, WuiAdmin.ksActionSchedGroupDetails, tsEffective,
+ { SchedGroupData.ksParam_idSchedGroup: oSchedGroup.idSchedGroup },
+ fBracketed = False,
+ sTitle = 'Scheduling group #%u' % (oSchedGroup.idSchedGroup,));
+ return WuiElementText('[scheduling group #%u not found]' % (idSchedGroup,));
+
+ def _createTestBoxDetailsLink(self, idTestBox, tsEffective):
+ """ Creates a link to the testbox details. """
+ oTestBox = self._oTestBoxLogic.cachedLookup(idTestBox);
+ if oTestBox is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oTestBox.sName, WuiAdmin.ksActionTestBoxDetails, tsEffective,
+ { TestBoxData.ksParam_idTestBox: oTestBox.idTestBox },
+ fBracketed = False, sTitle = 'Testbox #%u' % (oTestBox.idTestBox,));
+ return WuiElementText('[testbox #%u not found]' % (idTestBox,));
+
+ def _createTestCaseDetailsLink(self, idTestCase, tsEffective):
+ """ Creates a link to the test case details. """
+ oTestCase = self._oTestCaseLogic.cachedLookup(idTestCase);
+ if oTestCase is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oTestCase.sName, WuiAdmin.ksActionTestCaseDetails, tsEffective,
+ { TestCaseData.ksParam_idTestCase: oTestCase.idTestCase },
+ fBracketed = False, sTitle = 'Test case #%u' % (oTestCase.idTestCase,));
+ return WuiElementText('[test case #%u not found]' % (idTestCase,));
+
+ def _createTestGroupDetailsLink(self, idTestGroup, tsEffective):
+ """ Creates a link to the test group details. """
+ oTestGroup = self._oTestGroupLogic.cachedLookup(idTestGroup);
+ if oTestGroup is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oTestGroup.sName, WuiAdmin.ksActionTestGroupDetails, tsEffective,
+ { TestGroupData.ksParam_idTestGroup: oTestGroup.idTestGroup },
+ fBracketed = False, sTitle = 'Test group #%u' % (oTestGroup.idTestGroup,));
+ return WuiElementText('[test group #%u not found]' % (idTestGroup,));
+
+ def _createTestSetResultsDetailsLink(self, idTestSet, tsEffective):
+ """ Creates a link to the test set results. """
+ _ = tsEffective;
+ from testmanager.webui.wuimain import WuiMain;
+ return WuiMainLink('test set #%u' % idTestSet, WuiMain.ksActionTestSetDetails,
+ { TestSetData.ksParam_idTestSet: idTestSet }, fBracketed = False);
+
+ def _createTestSetDetailsLinkByResult(self, idTestResult, tsEffective):
+ """ Creates a link to the test set results. """
+ _ = tsEffective;
+ from testmanager.webui.wuimain import WuiMain;
+ return WuiMainLink('test result #%u' % idTestResult, WuiMain.ksActionTestSetDetailsFromResult,
+ { TestSetData.ksParam_idTestResult: idTestResult }, fBracketed = False);
+
+ def _createUserAccountDetailsLink(self, uid, tsEffective):
+ """ Creates a link to the user account details. """
+ oUser = self._oUserAccountLogic.cachedLookup(uid);
+ if oUser is not None:
+ return WuiAdminLink(oUser.sUsername, '@todo', tsEffective, { UserAccountData.ksParam_uid: oUser.uid },
+ fBracketed = False, sTitle = '%s (#%u)' % (oUser.sFullName, oUser.uid));
+ return WuiElementText('[user #%u not found]' % (uid,));
+
+ def _formatDescGeneric(self, sDesc, oEntry):
+ """
+ Generically format system log the description.
+ """
+ oRet = WuiHtmlKeeper();
+ asWords = sDesc.split();
+ for sWord in asWords:
+ offEqual = sWord.find('=');
+ if offEqual > 0:
+ sKey = sWord[:offEqual];
+ try: idValue = int(sWord[offEqual+1:].rstrip('.,'));
+ except: pass;
+ else:
+ if sKey == 'idTestSet':
+ oRet.append(self._createTestSetResultsDetailsLink(idValue, oEntry.tsEffective));
+ continue;
+ if sKey == 'idTestBox':
+ oRet.append(self._createTestBoxDetailsLink(idValue, oEntry.tsEffective));
+ continue;
+ if sKey == 'idSchedGroup':
+ oRet.append(self._createSchedGroupDetailsLink(idValue, oEntry.tsEffective));
+ continue;
+
+ oRet.append(WuiElementText(sWord));
+ return oRet;
+
+ def _formatListEntryHtml(self, iEntry): # pylint: disable=too-many-statements
+ """
+ Overridden parent method.
+ """
+ oEntry = self._aoEntries[iEntry];
+ sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven';
+ sHtml = u'';
+
+ #
+ # Format the timestamp.
+ #
+ sDate = self.formatTsShort(oEntry.tsEffective);
+ if sDate[:10] != self._sPrevDate:
+ self._sPrevDate = sDate[:10];
+ sHtml += ' <tr class="%s tmdaterow" align="left"><td colspan="7">%s</td></tr>\n' % (sRowClass, sDate[:10],);
+ sDate = sDate[11:]
+
+ #
+ # System log events.
+ # pylint: disable=redefined-variable-type
+ #
+ aoChanges = None;
+ if oEntry.sEvent == SystemLogData.ksEvent_CmdNacked:
+ sEvent = 'Command not acknowleged';
+ oDetails = oEntry.sDesc;
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_TestBoxUnknown:
+ sEvent = 'Unknown testbox';
+ oDetails = oEntry.sDesc;
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_TestSetAbandoned:
+ sEvent = 'Abandoned ' if oEntry.sDesc.startswith('idTestSet') else 'Abandoned test set';
+ oDetails = self._formatDescGeneric(oEntry.sDesc, oEntry);
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_UserAccountUnknown:
+ sEvent = 'Unknown user account';
+ oDetails = oEntry.sDesc;
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_XmlResultMalformed:
+ sEvent = 'Malformed XML result';
+ oDetails = oEntry.sDesc;
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_SchedQueueRecreate:
+ sEvent = 'Recreating scheduling queue';
+ asWords = oEntry.sDesc.split();
+ if len(asWords) > 3 and asWords[0] == 'User' and asWords[1][0] == '#':
+ try: idAuthor = int(asWords[1][1:]);
+ except: pass;
+ else:
+ oEntry.oAuthor = self._oUserAccountLogic.cachedLookup(idAuthor);
+ if oEntry.oAuthor is not None:
+ i = 2;
+ if asWords[i] == 'recreated': i += 1;
+ oEntry.sDesc = ' '.join(asWords[i:]);
+ oDetails = self._formatDescGeneric(oEntry.sDesc.replace('sched queue #', 'for scheduling group idSchedGroup='),
+ oEntry);
+ #
+ # System changelog events.
+ #
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_Blacklisting:
+ sEvent = 'Modified blacklisting';
+ oDetails = self._createBlacklistingDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_Build:
+ sEvent = 'Modified build';
+ oDetails = self._createBuildDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_BuildSource:
+ sEvent = 'Modified build source';
+ oDetails = self._createBuildSourceDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_GlobalRsrc:
+ sEvent = 'Modified global resource';
+ oDetails = self._createGlobalResourceDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_FailureCategory:
+ sEvent = 'Modified failure category';
+ oDetails = self._createFailureCategoryDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ (aoChanges, _) = self._oFailureCategoryLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_FailureReason:
+ sEvent = 'Modified failure reason';
+ oDetails = self._createFailureReasonDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ (aoChanges, _) = self._oFailureReasonLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_SchedGroup:
+ sEvent = 'Modified scheduling group';
+ oDetails = self._createSchedGroupDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestBox:
+ sEvent = 'Modified testbox';
+ oDetails = self._createTestBoxDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ (aoChanges, _) = self._oTestBoxLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestCase:
+ sEvent = 'Modified test case';
+ oDetails = self._createTestCaseDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ (aoChanges, _) = self._oTestCaseLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestGroup:
+ sEvent = 'Modified test group';
+ oDetails = self._createTestGroupDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestResult:
+ sEvent = 'Modified test failure reason';
+ oDetails = self._createTestSetDetailsLinkByResult(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_User:
+ sEvent = 'Modified user account';
+ oDetails = self._createUserAccountDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ else:
+ sEvent = '%s(%s)' % (oEntry.sEvent, oEntry.idWhat,);
+ oDetails = '!Unknown event!' + (oEntry.sDesc if oEntry.sDesc else '');
+
+ #
+ # Do the formatting.
+ #
+
+ if aoChanges:
+ oChangeEntry = aoChanges[0];
+ cAttribsChanged = len(oChangeEntry.aoChanges) + 1;
+ if oChangeEntry.oOldRaw is None and sEvent.startswith('Modified '):
+ sEvent = 'Created ' + sEvent[9:];
+
+ else:
+ oChangeEntry = None;
+ cAttribsChanged = -1;
+
+ sHtml += u' <tr class="%s">\n' \
+ u' <td rowspan="%d" align="center" >%s</td>\n' \
+ u' <td rowspan="%d" align="center" >%s</td>\n' \
+ u' <td colspan="5" class="%s%s">%s %s</td>\n' \
+ u' </tr>\n' \
+ % ( sRowClass,
+ 1 + cAttribsChanged + 1, sDate,
+ 1 + cAttribsChanged + 1, webutils.escapeElem(oEntry.oAuthor.sUsername if oEntry.oAuthor is not None else ''),
+ sRowClass, ' tmsyschlogevent' if oChangeEntry is not None else '', webutils.escapeElem(sEvent),
+ oDetails.toHtml() if isinstance(oDetails, WuiHtmlBase) else oDetails,
+ );
+
+ if oChangeEntry is not None:
+ sHtml += u' <tr class="%s tmsyschlogspacerrowabove">\n' \
+ u' <td xrowspan="%d" style="border-right: 0px; border-bottom: 0px;"></td>\n' \
+ u' <td colspan="3" style="border-right: 0px;"></td>\n' \
+ u' <td rowspan="%d" class="%s tmsyschlogspacer"></td>\n' \
+ u' </tr>\n' \
+ % (sRowClass, cAttribsChanged + 1, cAttribsChanged + 1, sRowClass);
+ for j, oChange in enumerate(oChangeEntry.aoChanges):
+ fLastRow = j + 1 == len(oChangeEntry.aoChanges);
+ sHtml += u' <tr class="%s%s tmsyschlogattr%s">\n' \
+ % ( sRowClass, 'odd' if j & 1 else 'even', ' tmsyschlogattrfinal' if fLastRow else '',);
+ if j == 0:
+ sHtml += u' <td class="%s tmsyschlogspacer" rowspan="%d"></td>\n' % (sRowClass, cAttribsChanged - 1,);
+
+ if isinstance(oChange, AttributeChangeEntryPre):
+ sHtml += u' <td class="%s%s">%s</td>\n' \
+ u' <td><div class="tdpre"><pre>%s</pre></div></td>\n' \
+ u' <td class="%s%s"><div class="tdpre"><pre>%s</pre></div></td>\n' \
+ % ( ' tmtopleft' if j == 0 else '', ' tmbottomleft' if fLastRow else '',
+ webutils.escapeElem(oChange.sAttr),
+ webutils.escapeElem(oChange.sOldText),
+ ' tmtopright' if j == 0 else '', ' tmbottomright' if fLastRow else '',
+ webutils.escapeElem(oChange.sNewText), );
+ else:
+ sHtml += u' <td class="%s%s">%s</td>\n' \
+ u' <td>%s</td>\n' \
+ u' <td class="%s%s">%s</td>\n' \
+ % ( ' tmtopleft' if j == 0 else '', ' tmbottomleft' if fLastRow else '',
+ webutils.escapeElem(oChange.sAttr),
+ webutils.escapeElem(oChange.sOldText),
+ ' tmtopright' if j == 0 else '', ' tmbottomright' if fLastRow else '',
+ webutils.escapeElem(oChange.sNewText), );
+ sHtml += u' </tr>\n';
+
+ if oChangeEntry is not None:
+ sHtml += u' <tr class="%s tmsyschlogspacerrowbelow "><td colspan="5"></td></tr>\n\n' % (sRowClass,);
+ return sHtml;
+
+
+ def _generateTableHeaders(self):
+ """
+ Overridden parent method.
+ """
+
+ sHtml = u'<thead class="tmheader">\n' \
+ u' <tr>\n' \
+ u' <th rowspan="2">When</th>\n' \
+ u' <th rowspan="2">Who</th>\n' \
+ u' <th colspan="5">Event</th>\n' \
+ u' </tr>\n' \
+ u' <tr>\n' \
+ u' <th style="border-right: 0px;"></th>\n' \
+ u' <th>Attribute</th>\n' \
+ u' <th>Old</th>\n' \
+ u' <th style="border-right: 0px;">New</th>\n' \
+ u' <th></th>\n' \
+ u' </tr>\n' \
+ u'</thead>\n';
+ return sHtml;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py
new file mode 100755
index 00000000..b0e6f99c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminsystemdbdump.py $
+
+"""
+Test Manager WUI - System DB - Partial Dumping
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase;
+from testmanager.webui.wuicontentbase import WuiFormContentBase;
+
+
+class WuiAdminSystemDbDumpForm(WuiFormContentBase):
+ """
+ WUI Partial DB Dump HTML content generator.
+ """
+
+ def __init__(self, cDaysBack, oDisp):
+ WuiFormContentBase.__init__(self, ModelDataBase(),
+ WuiFormContentBase.ksMode_Edit, 'DbDump', oDisp, 'Partial DB Dump',
+ sSubmitAction = oDisp.ksActionSystemDbDumpDownload);
+ self._cDaysBack = cDaysBack;
+
+ def _generateTopRowFormActions(self, oData):
+ _ = oData;
+ return [];
+
+ def _populateForm(self, oForm, oData): # type: (WuiHlpForm, SchedGroupDataEx) -> bool
+ """
+ Construct an HTML form
+ """
+ _ = oData;
+
+ oForm.addInt(self._oDisp.ksParamDaysBack, self._cDaysBack, 'How many days back to dump');
+ oForm.addSubmit('Produce & Download');
+
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py
new file mode 100755
index 00000000..6b3b7a45
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminsystemlog.py $
+
+"""
+Test Manager WUI - Admin - System Log.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiListContentBase, WuiTmLink;
+from testmanager.core.testbox import TestBoxData;
+from testmanager.core.systemlog import SystemLogData;
+from testmanager.core.useraccount import UserAccountData;
+
+
+class WuiAdminSystemLogList(WuiListContentBase):
+ """
+ WUI System Log Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'System Log',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = ['Date', 'Event', 'Message', 'Action'];
+ self._asColumnAttribs = ['', '', '', 'align="center"'];
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ oEntry = self._aoEntries[iEntry];
+
+ oAction = None
+
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ if oEntry.sEvent == SystemLogData.ksEvent_TestBoxUnknown \
+ and oEntry.sLogText.find('addr=') >= 0 \
+ and oEntry.sLogText.find('uuid=') >= 0:
+ sUuid = (oEntry.sLogText[(oEntry.sLogText.find('uuid=') + 5):])[:36];
+ sAddr = (oEntry.sLogText[(oEntry.sLogText.find('addr=') + 5):]).split(' ')[0];
+ oAction = WuiTmLink('Add TestBox', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxAdd,
+ TestBoxData.ksParam_uuidSystem: sUuid,
+ TestBoxData.ksParam_ip: sAddr });
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_UserAccountUnknown:
+ sUserName = oEntry.sLogText[oEntry.sLogText.find('(') + 1:
+ oEntry.sLogText.find(')')]
+ oAction = WuiTmLink('Add User', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserAdd,
+ UserAccountData.ksParam_sLoginName: sUserName });
+
+ return [oEntry.tsCreated, oEntry.sEvent, oEntry.sLogText, oAction];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py
new file mode 100755
index 00000000..a06e670c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py
@@ -0,0 +1,490 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadmintestbox.py $
+
+"""
+Test Manager WUI - TestBox.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import socket;
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuicontentbase import WuiContentBase, WuiListContentWithActionBase, WuiFormContentBase, WuiLinkBase, \
+ WuiSvnLink, WuiTmLink, WuiSpanText, WuiRawHtml;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.schedgroup import SchedGroupLogic, SchedGroupData;
+from testmanager.core.testbox import TestBoxData, TestBoxDataEx, TestBoxLogic;
+from testmanager.core.testset import TestSetData;
+from testmanager.core.db import isDbTimestampInfinity;
+
+
+
+class WuiTestBoxDetailsLinkById(WuiTmLink):
+ """ Test box details link by ID. """
+
+ def __init__(self, idTestBox, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, tsNow = None, sTitle = None):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ dParams = {
+ WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails,
+ TestBoxData.ksParam_idTestBox: idTestBox,
+ };
+ if tsNow is not None:
+ dParams[WuiAdmin.ksParamEffectiveDate] = tsNow; ## ??
+ WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams, fBracketed = fBracketed, sTitle = sTitle);
+ self.idTestBox = idTestBox;
+
+
+class WuiTestBoxDetailsLink(WuiTestBoxDetailsLinkById):
+ """ Test box details link by TestBoxData instance. """
+
+ def __init__(self, oTestBox, sName = None, fBracketed = False, tsNow = None): # (TestBoxData, str, bool, Any) -> None
+ WuiTestBoxDetailsLinkById.__init__(self, oTestBox.idTestBox,
+ sName if sName else oTestBox.sName,
+ fBracketed = fBracketed,
+ tsNow = tsNow,
+ sTitle = self.formatTitleText(oTestBox));
+ self.oTestBox = oTestBox;
+
+ @staticmethod
+ def formatTitleText(oTestBox): # (TestBoxData) -> str
+ """
+ Formats the title text for a TestBoxData object.
+ """
+
+ # Note! Somewhat similar code is found in testresults.py
+
+ #
+ # Collect field/value tuples.
+ #
+ aasTestBoxTitle = [
+ (u'Identifier:', '#%u' % (oTestBox.idTestBox,),),
+ (u'Name:', oTestBox.sName,),
+ ];
+ if oTestBox.sCpuVendor:
+ aasTestBoxTitle.append((u'CPU\u00a0vendor:', oTestBox.sCpuVendor, ));
+ if oTestBox.sCpuName:
+ aasTestBoxTitle.append((u'CPU\u00a0name:', u'\u00a0'.join(oTestBox.sCpuName.split()),));
+ if oTestBox.cCpus:
+ aasTestBoxTitle.append((u'CPU\u00a0threads:', u'%s' % ( oTestBox.cCpus, ),));
+
+ asFeatures = [];
+ if oTestBox.fCpuHwVirt is True:
+ if oTestBox.sCpuVendor is None:
+ asFeatures.append(u'HW\u2011Virt');
+ elif oTestBox.sCpuVendor in ['AuthenticAMD',]:
+ asFeatures.append(u'HW\u2011Virt(AMD\u2011V)');
+ else:
+ asFeatures.append(u'HW\u2011Virt(VT\u2011x)');
+ if oTestBox.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging');
+ if oTestBox.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest');
+ if oTestBox.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU');
+ aasTestBoxTitle.append((u'CPU\u00a0features:', u',\u00a0'.join(asFeatures),));
+
+ if oTestBox.cMbMemory:
+ aasTestBoxTitle.append((u'System\u00a0RAM:', u'%s MiB' % ( oTestBox.cMbMemory, ),));
+ if oTestBox.sOs:
+ aasTestBoxTitle.append((u'OS:', oTestBox.sOs, ));
+ if oTestBox.sCpuArch:
+ aasTestBoxTitle.append((u'OS\u00a0arch:', oTestBox.sCpuArch,));
+ if oTestBox.sOsVersion:
+ aasTestBoxTitle.append((u'OS\u00a0version:', u'\u00a0'.join(oTestBox.sOsVersion.split()),));
+ if oTestBox.ip:
+ aasTestBoxTitle.append((u'IP\u00a0address:', u'%s' % ( oTestBox.ip, ),));
+
+ #
+ # Do a guestimation of the max field name width and pad short
+ # names when constructing the title text lines.
+ #
+ cchMaxWidth = 0;
+ for sEntry, _ in aasTestBoxTitle:
+ cchMaxWidth = max(WuiTestBoxDetailsLink.estimateStringWidth(sEntry), cchMaxWidth);
+ asTestBoxTitle = [];
+ for sEntry, sValue in aasTestBoxTitle:
+ asTestBoxTitle.append(u'%s%s\t\t%s'
+ % (sEntry, WuiTestBoxDetailsLink.getStringWidthPadding(sEntry, cchMaxWidth), sValue));
+
+ return u'\n'.join(asTestBoxTitle);
+
+
+class WuiTestBoxDetailsLinkShort(WuiTestBoxDetailsLink):
+ """ Test box details link by TestBoxData instance, but with ksShortDetailsLink as default name. """
+
+ def __init__(self, oTestBox, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False,
+ tsNow = None): # (TestBoxData, str, bool, Any) -> None
+ WuiTestBoxDetailsLink.__init__(self, oTestBox, sName = sName, fBracketed = fBracketed, tsNow = tsNow);
+
+
+class WuiTestBox(WuiFormContentBase):
+ """
+ WUI TestBox Form Content Generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Create TextBox';
+ if oData.uuidSystem is not None and len(oData.uuidSystem) > 10:
+ sTitle += ' - ' + oData.uuidSystem;
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit TestBox - %s (#%s)' % (oData.sName, oData.idTestBox);
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'TestBox - %s (#%s)' % (oData.sName, oData.idTestBox);
+ WuiFormContentBase.__init__(self, oData, sMode, 'TestBox', oDisp, sTitle);
+
+ # Try enter sName as hostname (no domain) when creating the testbox.
+ if sMode == WuiFormContentBase.ksMode_Add \
+ and self._oData.sName in [None, ''] \
+ and self._oData.ip not in [None, '']:
+ try:
+ (self._oData.sName, _, _) = socket.gethostbyaddr(self._oData.ip);
+ except:
+ pass;
+ offDot = self._oData.sName.find('.');
+ if offDot > 0:
+ self._oData.sName = self._oData.sName[:offDot];
+
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO( TestBoxData.ksParam_idTestBox, oData.idTestBox, 'TestBox ID');
+ oForm.addIntRO( TestBoxData.ksParam_idGenTestBox, oData.idGenTestBox, 'TestBox generation ID');
+ oForm.addTimestampRO(TestBoxData.ksParam_tsEffective, oData.tsEffective, 'Last changed');
+ oForm.addTimestampRO(TestBoxData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)');
+ oForm.addIntRO( TestBoxData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID');
+
+ oForm.addText( TestBoxData.ksParam_ip, oData.ip, 'TestBox IP Address'); ## make read only??
+ oForm.addUuid( TestBoxData.ksParam_uuidSystem, oData.uuidSystem, 'TestBox System/Firmware UUID');
+ oForm.addText( TestBoxData.ksParam_sName, oData.sName, 'TestBox Name');
+ oForm.addText( TestBoxData.ksParam_sDescription, oData.sDescription, 'TestBox Description');
+ oForm.addCheckBox( TestBoxData.ksParam_fEnabled, oData.fEnabled, 'Enabled');
+ oForm.addComboBox( TestBoxData.ksParam_enmLomKind, oData.enmLomKind, 'Lights-out-management',
+ TestBoxData.kaoLomKindDescs);
+ oForm.addText( TestBoxData.ksParam_ipLom, oData.ipLom, 'Lights-out-management IP Address');
+ oForm.addInt( TestBoxData.ksParam_pctScaleTimeout, oData.pctScaleTimeout, 'Timeout scale factor (%)');
+
+ oForm.addListOfSchedGroupsForTestBox(TestBoxDataEx.ksParam_aoInSchedGroups,
+ oData.aoInSchedGroups,
+ SchedGroupLogic(TMDatabaseConnection()).fetchOrderedByName(),
+ 'Scheduling Group', oData.idTestBox);
+ # Command, comment and submit button.
+ if self._sMode == WuiFormContentBase.ksMode_Edit:
+ oForm.addComboBox(TestBoxData.ksParam_enmPendingCmd, oData.enmPendingCmd, 'Pending command',
+ TestBoxData.kaoTestBoxCmdDescs);
+ else:
+ oForm.addComboBoxRO(TestBoxData.ksParam_enmPendingCmd, oData.enmPendingCmd, 'Pending command',
+ TestBoxData.kaoTestBoxCmdDescs);
+ oForm.addMultilineText(TestBoxData.ksParam_sComment, oData.sComment, 'Comment');
+ if self._sMode != WuiFormContentBase.ksMode_Show:
+ oForm.addSubmit('Create TestBox' if self._sMode == WuiFormContentBase.ksMode_Add else 'Change TestBox');
+
+ return True;
+
+
+ def _generatePostFormContent(self, oData):
+ from testmanager.webui.wuihlpform import WuiHlpForm;
+
+ oForm = WuiHlpForm('testbox-machine-settable', '', fReadOnly = True);
+ oForm.addTextRO( TestBoxData.ksParam_sOs, oData.sOs, 'TestBox OS');
+ oForm.addTextRO( TestBoxData.ksParam_sOsVersion, oData.sOsVersion, 'TestBox OS version');
+ oForm.addTextRO( TestBoxData.ksParam_sCpuArch, oData.sCpuArch, 'TestBox OS kernel architecture');
+ oForm.addTextRO( TestBoxData.ksParam_sCpuVendor, oData.sCpuVendor, 'TestBox CPU vendor');
+ oForm.addTextRO( TestBoxData.ksParam_sCpuName, oData.sCpuName, 'TestBox CPU name');
+ if oData.lCpuRevision:
+ oForm.addTextRO( TestBoxData.ksParam_lCpuRevision, '%#x' % (oData.lCpuRevision,), 'TestBox CPU revision',
+ sPostHtml = ' (family=%#x model=%#x stepping=%#x)'
+ % (oData.getCpuFamily(), oData.getCpuModel(), oData.getCpuStepping(),),
+ sSubClass = 'long');
+ else:
+ oForm.addLongRO( TestBoxData.ksParam_lCpuRevision, oData.lCpuRevision, 'TestBox CPU revision');
+ oForm.addIntRO( TestBoxData.ksParam_cCpus, oData.cCpus, 'Number of CPUs, cores and threads');
+ oForm.addCheckBoxRO( TestBoxData.ksParam_fCpuHwVirt, oData.fCpuHwVirt, 'VT-x or AMD-V supported');
+ oForm.addCheckBoxRO( TestBoxData.ksParam_fCpuNestedPaging, oData.fCpuNestedPaging, 'Nested paging supported');
+ oForm.addCheckBoxRO( TestBoxData.ksParam_fCpu64BitGuest, oData.fCpu64BitGuest, '64-bit guest supported');
+ oForm.addCheckBoxRO( TestBoxData.ksParam_fChipsetIoMmu, oData.fChipsetIoMmu, 'I/O MMU supported');
+ oForm.addMultilineTextRO(TestBoxData.ksParam_sReport, oData.sReport, 'Hardware/software report');
+ oForm.addLongRO( TestBoxData.ksParam_cMbMemory, oData.cMbMemory, 'Installed RAM size (MB)');
+ oForm.addLongRO( TestBoxData.ksParam_cMbScratch, oData.cMbScratch, 'Available scratch space (MB)');
+ oForm.addIntRO( TestBoxData.ksParam_iTestBoxScriptRev, oData.iTestBoxScriptRev,
+ 'TestBox Script SVN revision');
+ sHexVer = oData.formatPythonVersion();
+ oForm.addIntRO( TestBoxData.ksParam_iPythonHexVersion, oData.iPythonHexVersion,
+ 'Python version (hex)', sPostHtml = webutils.escapeElem(sHexVer));
+ return [('Machine Only Settables', oForm.finalize()),];
+
+
+
+class WuiTestBoxList(WuiListContentWithActionBase):
+ """
+ WUI TestBox List Content Generator.
+ """
+
+ ## Descriptors for the combo box.
+ kasTestBoxActionDescs = \
+ [ \
+ [ 'none', 'Select an action...', '' ],
+ [ 'enable', 'Enable', '' ],
+ [ 'disable', 'Disable', '' ],
+ TestBoxData.kaoTestBoxCmdDescs[1],
+ TestBoxData.kaoTestBoxCmdDescs[2],
+ TestBoxData.kaoTestBoxCmdDescs[3],
+ TestBoxData.kaoTestBoxCmdDescs[4],
+ TestBoxData.kaoTestBoxCmdDescs[5],
+ ];
+
+ ## Boxes which doesn't report in for more than 15 min are considered dead.
+ kcSecMaxStatusDeltaAlive = 15*60
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ # type: (list[TestBoxDataForListing], int, int, datetime.datetime, ignore, WuiAdmin) -> None
+ WuiListContentWithActionBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'TestBoxes', sId = 'users', fnDPrint = fnDPrint, oDisp = oDisp,
+ aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders.extend([ 'Name', 'LOM', 'Status', 'Cmd',
+ 'Note', 'Script', 'Python', 'Group',
+ 'OS', 'CPU', 'Features', 'CPUs', 'RAM', 'Scratch',
+ 'Actions' ]);
+ self._asColumnAttribs.extend([ 'align="center"', 'align="center"', 'align="center"', 'align="center"'
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ '', '', '', 'align="left"', 'align="right"', 'align="right"', 'align="right"',
+ 'align="center"' ]);
+ self._aaiColumnSorting.extend([
+ (TestBoxLogic.kiSortColumn_sName,),
+ None, # LOM
+ (-TestBoxLogic.kiSortColumn_fEnabled, TestBoxLogic.kiSortColumn_enmState, -TestBoxLogic.kiSortColumn_tsUpdated,),
+ (TestBoxLogic.kiSortColumn_enmPendingCmd,),
+ None, # Note
+ (TestBoxLogic.kiSortColumn_iTestBoxScriptRev,),
+ (TestBoxLogic.kiSortColumn_iPythonHexVersion,),
+ None, # Group
+ (TestBoxLogic.kiSortColumn_sOs, TestBoxLogic.kiSortColumn_sOsVersion, TestBoxLogic.kiSortColumn_sCpuArch,),
+ (TestBoxLogic.kiSortColumn_sCpuVendor, TestBoxLogic.kiSortColumn_lCpuRevision,),
+ (TestBoxLogic.kiSortColumn_fCpuNestedPaging,),
+ (TestBoxLogic.kiSortColumn_cCpus,),
+ (TestBoxLogic.kiSortColumn_cMbMemory,),
+ (TestBoxLogic.kiSortColumn_cMbScratch,),
+ None, # Actions
+ ]);
+ assert len(self._aaiColumnSorting) == len(self._asColumnHeaders);
+ self._aoActions = list(self.kasTestBoxActionDescs);
+ self._sAction = oDisp.ksActionTestBoxListPost;
+ self._sCheckboxName = TestBoxData.ksParam_idTestBox;
+
+ def show(self, fShowNavigation = True):
+ """ Adds some stats at the bottom of the page """
+ (sTitle, sBody) = super(WuiTestBoxList, self).show(fShowNavigation);
+
+ # Count boxes in interesting states.
+ if self._aoEntries:
+ cActive = 0;
+ cDead = 0;
+ for oTestBox in self._aoEntries:
+ if oTestBox.oStatus is not None:
+ oDelta = oTestBox.tsCurrent - oTestBox.oStatus.tsUpdated;
+ if oDelta.days <= 0 and oDelta.seconds <= self.kcSecMaxStatusDeltaAlive:
+ if oTestBox.fEnabled:
+ cActive += 1;
+ else:
+ cDead += 1;
+ else:
+ cDead += 1;
+ sBody += '<div id="testboxsummary"><p>\n' \
+ '%s testboxes of which %s are active and %s dead' \
+ '</p></div>\n' \
+ % (len(self._aoEntries), cActive, cDead,)
+ return (sTitle, sBody);
+
+ def _formatListEntry(self, iEntry): # pylint: disable=too-many-locals
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ oEntry = self._aoEntries[iEntry];
+
+ # Lights outs managment.
+ if oEntry.enmLomKind == TestBoxData.ksLomKind_ILOM:
+ aoLom = [ WuiLinkBase('ILOM', 'https://%s/' % (oEntry.ipLom,), fBracketed = False), ];
+ elif oEntry.enmLomKind == TestBoxData.ksLomKind_ELOM:
+ aoLom = [ WuiLinkBase('ELOM', 'http://%s/' % (oEntry.ipLom,), fBracketed = False), ];
+ elif oEntry.enmLomKind == TestBoxData.ksLomKind_AppleXserveLom:
+ aoLom = [ 'Apple LOM' ];
+ elif oEntry.enmLomKind == TestBoxData.ksLomKind_None:
+ aoLom = [ 'none' ];
+ else:
+ aoLom = [ 'Unexpected enmLomKind value "%s"' % (oEntry.enmLomKind,) ];
+ if oEntry.ipLom is not None:
+ if oEntry.enmLomKind in [ TestBoxData.ksLomKind_ILOM, TestBoxData.ksLomKind_ELOM ]:
+ aoLom += [ WuiLinkBase('(ssh)', 'ssh://%s' % (oEntry.ipLom,), fBracketed = False) ];
+ aoLom += [ WuiRawHtml('<br>'), '%s' % (oEntry.ipLom,) ];
+
+ # State and Last seen.
+ if oEntry.oStatus is None:
+ oSeen = WuiSpanText('tmspan-offline', 'Never');
+ oState = '';
+ else:
+ oDelta = oEntry.tsCurrent - oEntry.oStatus.tsUpdated;
+ if oDelta.days <= 0 and oDelta.seconds <= self.kcSecMaxStatusDeltaAlive:
+ oSeen = WuiSpanText('tmspan-online', u'%s\u00a0s\u00a0ago' % (oDelta.days * 24 * 3600 + oDelta.seconds,));
+ else:
+ oSeen = WuiSpanText('tmspan-offline', u'%s' % (self.formatTsShort(oEntry.oStatus.tsUpdated),));
+
+ if oEntry.oStatus.idTestSet is None:
+ oState = str(oEntry.oStatus.enmState);
+ else:
+ from testmanager.webui.wuimain import WuiMain;
+ oState = WuiTmLink(oEntry.oStatus.enmState, WuiMain.ksScriptName, # pylint: disable=redefined-variable-type
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: oEntry.oStatus.idTestSet, },
+ sTitle = '#%u' % (oEntry.oStatus.idTestSet,),
+ fBracketed = False);
+ # Comment
+ oComment = self._formatCommentCell(oEntry.sComment);
+
+ # Group links.
+ aoGroups = [];
+ for oInGroup in oEntry.aoInSchedGroups:
+ oSchedGroup = oInGroup.oSchedGroup;
+ aoGroups.append(WuiTmLink(oSchedGroup.sName, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupEdit,
+ SchedGroupData.ksParam_idSchedGroup: oSchedGroup.idSchedGroup, },
+ sTitle = '#%u' % (oSchedGroup.idSchedGroup,),
+ fBracketed = len(oEntry.aoInSchedGroups) > 1));
+
+ # Reformat the OS version to take less space.
+ aoOs = [ 'N/A' ];
+ if oEntry.sOs is not None and oEntry.sOsVersion is not None and oEntry.sCpuArch:
+ sOsVersion = oEntry.sOsVersion;
+ if sOsVersion[0] not in [ 'v', 'V', 'r', 'R'] \
+ and sOsVersion[0].isdigit() \
+ and sOsVersion.find('.') in range(4) \
+ and oEntry.sOs in [ 'linux', 'solaris', 'darwin', ]:
+ sOsVersion = 'v' + sOsVersion;
+
+ sVer1 = sOsVersion;
+ sVer2 = None;
+ if oEntry.sOs in ('linux', 'darwin'):
+ iSep = sOsVersion.find(' / ');
+ if iSep > 0:
+ sVer1 = sOsVersion[:iSep].strip();
+ sVer2 = sOsVersion[iSep + 3:].strip();
+ sVer2 = sVer2.replace('Red Hat Enterprise Linux Server', 'RHEL');
+ sVer2 = sVer2.replace('Oracle Linux Server', 'OL');
+ elif oEntry.sOs == 'solaris':
+ iSep = sOsVersion.find(' (');
+ if iSep > 0 and sOsVersion[-1] == ')':
+ sVer1 = sOsVersion[:iSep].strip();
+ sVer2 = sOsVersion[iSep + 2:-1].strip();
+ elif oEntry.sOs == 'win':
+ iSep = sOsVersion.find('build');
+ if iSep > 0:
+ sVer1 = sOsVersion[:iSep].strip();
+ sVer2 = 'B' + sOsVersion[iSep + 1:].strip();
+ aoOs = [
+ WuiSpanText('tmspan-osarch', u'%s.%s' % (oEntry.sOs, oEntry.sCpuArch,)),
+ WuiSpanText('tmspan-osver1', sVer1.replace('-', u'\u2011'),),
+ ];
+ if sVer2 is not None:
+ aoOs += [ WuiRawHtml('<br>'), WuiSpanText('tmspan-osver2', sVer2.replace('-', u'\u2011')), ];
+
+ # Format the CPU revision.
+ oCpu = None;
+ if oEntry.lCpuRevision is not None and oEntry.sCpuVendor is not None and oEntry.sCpuName is not None:
+ oCpu = [
+ u'%s (fam:%xh\u00a0m:%xh\u00a0s:%xh)'
+ % (oEntry.sCpuVendor, oEntry.getCpuFamily(), oEntry.getCpuModel(), oEntry.getCpuStepping(),),
+ WuiRawHtml('<br>'),
+ oEntry.sCpuName,
+ ];
+ else:
+ oCpu = [];
+ if oEntry.sCpuVendor is not None:
+ oCpu.append(oEntry.sCpuVendor);
+ if oEntry.lCpuRevision is not None:
+ oCpu.append('%#x' % (oEntry.lCpuRevision,));
+ if oEntry.sCpuName is not None:
+ oCpu.append(oEntry.sCpuName);
+
+ # Stuff cpu vendor and cpu/box features into one field.
+ asFeatures = []
+ if oEntry.fCpuHwVirt is True: asFeatures.append(u'HW\u2011Virt');
+ if oEntry.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging');
+ if oEntry.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest');
+ if oEntry.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU');
+ sFeatures = u' '.join(asFeatures) if asFeatures else u'';
+
+ # Collection applicable actions.
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails,
+ TestBoxData.ksParam_idTestBox: oEntry.idTestBox,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } ),
+ ]
+
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions += [
+ WuiTmLink('Edit', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxEdit,
+ TestBoxData.ksParam_idTestBox: oEntry.idTestBox, } ),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxRemovePost,
+ TestBoxData.ksParam_idTestBox: oEntry.idTestBox },
+ sConfirm = 'Are you sure that you want to remove %s (%s)?' % (oEntry.sName, oEntry.ip) ),
+ ]
+
+ if oEntry.sOs not in [ 'win', 'os2', ] and oEntry.ip is not None:
+ aoActions.append(WuiLinkBase('ssh', 'ssh://vbox@%s' % (oEntry.ip,),));
+
+ return [ self._getCheckBoxColumn(iEntry, oEntry.idTestBox),
+ [ WuiSpanText('tmspan-name', oEntry.sName), WuiRawHtml('<br>'), '%s' % (oEntry.ip,),],
+ aoLom,
+ [
+ '' if oEntry.fEnabled else 'disabled / ',
+ oState,
+ WuiRawHtml('<br>'),
+ oSeen,
+ ],
+ oEntry.enmPendingCmd,
+ oComment,
+ WuiSvnLink(oEntry.iTestBoxScriptRev),
+ oEntry.formatPythonVersion(),
+ aoGroups,
+ aoOs,
+ oCpu,
+ sFeatures,
+ oEntry.cCpus if oEntry.cCpus is not None else 'N/A',
+ utils.formatNumberNbsp(oEntry.cMbMemory) + u'\u00a0MB' if oEntry.cMbMemory is not None else 'N/A',
+ utils.formatNumberNbsp(oEntry.cMbScratch) + u'\u00a0MB' if oEntry.cMbScratch is not None else 'N/A',
+ aoActions,
+ ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py
new file mode 100755
index 00000000..19fa86c9
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadmintestcase.py $
+
+"""
+Test Manager WUI - Test Cases.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiContentBase, WuiTmLink, WuiRawHtml;
+from testmanager.core.db import isDbTimestampInfinity;
+from testmanager.core.testcase import TestCaseDataEx, TestCaseData, TestCaseDependencyLogic;
+from testmanager.core.globalresource import GlobalResourceData, GlobalResourceLogic;
+
+
+
+class WuiTestCaseDetailsLink(WuiTmLink):
+ """ Test case details link by ID. """
+
+ def __init__(self, idTestCase, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, tsNow = None):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ dParams = {
+ WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idTestCase: idTestCase,
+ };
+ if tsNow is not None:
+ dParams[WuiAdmin.ksParamEffectiveDate] = tsNow; ## ??
+ WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams, fBracketed = fBracketed);
+ self.idTestCase = idTestCase;
+
+
+class WuiTestCaseList(WuiListContentBase):
+ """
+ WUI test case list content generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, sTitle = 'Test Cases',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = \
+ [
+ 'Name', 'Active', 'Timeout', 'Base Command / Variations', 'Validation Kit Files',
+ 'Test Case Prereqs', 'Global Rsrces', 'Note', 'Actions'
+ ];
+ self._asColumnAttribs = \
+ [
+ '', '', 'align="center"', '', '',
+ 'valign="top"', 'valign="top"', 'align="center"', 'align="center"'
+ ];
+
+ def _formatListEntry(self, iEntry):
+ oEntry = self._aoEntries[iEntry];
+ from testmanager.webui.wuiadmin import WuiAdmin;
+
+ aoRet = \
+ [
+ oEntry.sName.replace('-', u'\u2011'),
+ 'Enabled' if oEntry.fEnabled else 'Disabled',
+ utils.formatIntervalSeconds(oEntry.cSecTimeout),
+ ];
+
+ # Base command and variations.
+ fNoGang = True;
+ fNoSubName = True;
+ fAllDefaultTimeouts = True;
+ for oVar in oEntry.aoTestCaseArgs:
+ if fNoSubName and oVar.sSubName is not None and oVar.sSubName.strip():
+ fNoSubName = False;
+ if oVar.cGangMembers > 1:
+ fNoGang = False;
+ if oVar.cSecTimeout is not None:
+ fAllDefaultTimeouts = False;
+
+ sHtml = ' <table class="tminnertbl" width=100%>\n' \
+ ' <tr>\n' \
+ ' ';
+ if not fNoSubName:
+ sHtml += '<th class="tmtcasubname">Sub-name</th>';
+ if not fNoGang:
+ sHtml += '<th class="tmtcagangsize">Gang Size</th>';
+ if not fAllDefaultTimeouts:
+ sHtml += '<th class="tmtcatimeout">Timeout</th>';
+ sHtml += '<th>Additional Arguments</b></th>\n' \
+ ' </tr>\n'
+ for oTmp in oEntry.aoTestCaseArgs:
+ sHtml += '<tr>';
+ if not fNoSubName:
+ sHtml += '<td>%s</td>' % (webutils.escapeElem(oTmp.sSubName) if oTmp.sSubName is not None else '');
+ if not fNoGang:
+ sHtml += '<td>%d</td>' % (oTmp.cGangMembers,)
+ if not fAllDefaultTimeouts:
+ sHtml += '<td>%s</td>' \
+ % (utils.formatIntervalSeconds(oTmp.cSecTimeout) if oTmp.cSecTimeout is not None else 'Default',)
+ sHtml += u'<td>%s</td></tr>' \
+ % ( webutils.escapeElem(oTmp.sArgs.replace('-', u'\u2011')) if oTmp.sArgs else u'\u2011',);
+ sHtml += '</tr>\n';
+ sHtml += ' </table>'
+
+ aoRet.append([oEntry.sBaseCmd.replace('-', u'\u2011'), WuiRawHtml(sHtml)]);
+
+ # Next.
+ aoRet += [ oEntry.sValidationKitZips if oEntry.sValidationKitZips is not None else '', ];
+
+ # Show dependency on other testcases
+ if oEntry.aoDepTestCases not in (None, []):
+ sHtml = ' <ul class="tmshowall">\n'
+ for sTmp in oEntry.aoDepTestCases:
+ sHtml += ' <li class="tmshowall"><a href="%s?%s=%s&%s=%s">%s</a></li>\n' \
+ % (WuiAdmin.ksScriptName,
+ WuiAdmin.ksParamAction, WuiAdmin.ksActionTestCaseEdit,
+ TestCaseData.ksParam_idTestCase, sTmp.idTestCase,
+ sTmp.sName)
+ sHtml += ' </ul>\n'
+ else:
+ sHtml = '<ul class="tmshowall"><li class="tmshowall">None</li></ul>\n'
+ aoRet.append(WuiRawHtml(sHtml));
+
+ # Show dependency on global resources
+ if oEntry.aoDepGlobalResources not in (None, []):
+ sHtml = ' <ul class="tmshowall">\n'
+ for sTmp in oEntry.aoDepGlobalResources:
+ sHtml += ' <li class="tmshowall"><a href="%s?%s=%s&%s=%s">%s</a></li>\n' \
+ % (WuiAdmin.ksScriptName,
+ WuiAdmin.ksParamAction, WuiAdmin.ksActionGlobalRsrcShowEdit,
+ GlobalResourceData.ksParam_idGlobalRsrc, sTmp.idGlobalRsrc,
+ sTmp.sName)
+ sHtml += ' </ul>\n'
+ else:
+ sHtml = '<ul class="tmshowall"><li class="tmshowall">None</li></ul>\n'
+ aoRet.append(WuiRawHtml(sHtml));
+
+ # Comment (note).
+ aoRet.append(self._formatCommentCell(oEntry.sComment));
+
+ # Show actions that can be taken.
+ aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idGenTestCase: oEntry.idGenTestCase }), ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseEdit,
+ TestCaseData.ksParam_idTestCase: oEntry.idTestCase }));
+ aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseClone,
+ TestCaseData.ksParam_idGenTestCase: oEntry.idGenTestCase }));
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDoRemove,
+ TestCaseData.ksParam_idTestCase: oEntry.idTestCase },
+ sConfirm = 'Are you sure you want to remove test case #%d?' % (oEntry.idTestCase,)));
+ aoRet.append(aoActions);
+
+ return aoRet;
+
+
+class WuiTestCase(WuiFormContentBase):
+ """
+ WUI user account content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ assert isinstance(oData, TestCaseDataEx);
+
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'New Test Case';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit Test Case - %s (#%s)' % (oData.sName, oData.idTestCase);
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Test Case - %s (#%s)' % (oData.sName, oData.idTestCase);
+ WuiFormContentBase.__init__(self, oData, sMode, 'TestCase', oDisp, sTitle);
+
+ # Read additional bits form the DB.
+ oDepLogic = TestCaseDependencyLogic(oDisp.getDb());
+ self._aoAllTestCases = oDepLogic.getApplicableDepTestCaseData(-1 if oData.idTestCase is None else oData.idTestCase);
+ self._aoAllGlobalRsrcs = GlobalResourceLogic(oDisp.getDb()).getAll();
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO (TestCaseData.ksParam_idTestCase, oData.idTestCase, 'Test Case ID')
+ oForm.addTimestampRO(TestCaseData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(TestCaseData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (TestCaseData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+ oForm.addIntRO (TestCaseData.ksParam_idGenTestCase, oData.idGenTestCase, 'Test Case generation ID')
+ oForm.addText (TestCaseData.ksParam_sName, oData.sName, 'Name')
+ oForm.addText (TestCaseData.ksParam_sDescription, oData.sDescription, 'Description')
+ oForm.addCheckBox (TestCaseData.ksParam_fEnabled, oData.fEnabled, 'Enabled')
+ oForm.addLong (TestCaseData.ksParam_cSecTimeout,
+ utils.formatIntervalSeconds2(oData.cSecTimeout), 'Default timeout')
+ oForm.addWideText (TestCaseData.ksParam_sTestBoxReqExpr, oData.sTestBoxReqExpr, 'TestBox requirements (python)');
+ oForm.addWideText (TestCaseData.ksParam_sBuildReqExpr, oData.sBuildReqExpr, 'Build requirement (python)');
+ oForm.addWideText (TestCaseData.ksParam_sBaseCmd, oData.sBaseCmd, 'Base command')
+ oForm.addText (TestCaseData.ksParam_sValidationKitZips, oData.sValidationKitZips, 'Test suite files')
+
+ oForm.addListOfTestCaseArgs(TestCaseDataEx.ksParam_aoTestCaseArgs, oData.aoTestCaseArgs, 'Argument variations')
+
+ aoTestCaseDeps = [];
+ for oTestCase in self._aoAllTestCases:
+ if oTestCase.idTestCase == oData.idTestCase:
+ continue;
+ fSelected = False;
+ for oDep in oData.aoDepTestCases:
+ if oDep.idTestCase == oTestCase.idTestCase:
+ fSelected = True;
+ break;
+ aoTestCaseDeps.append([oTestCase.idTestCase, fSelected, oTestCase.sName]);
+ oForm.addListOfTestCases(TestCaseDataEx.ksParam_aoDepTestCases, aoTestCaseDeps, 'Depends on test cases')
+
+ aoGlobalResrcDeps = [];
+ for oGlobalRsrc in self._aoAllGlobalRsrcs:
+ fSelected = False;
+ for oDep in oData.aoDepGlobalResources:
+ if oDep.idGlobalRsrc == oGlobalRsrc.idGlobalRsrc:
+ fSelected = True;
+ break;
+ aoGlobalResrcDeps.append([oGlobalRsrc.idGlobalRsrc, fSelected, oGlobalRsrc.sName]);
+ oForm.addListOfResources(TestCaseDataEx.ksParam_aoDepGlobalResources, aoGlobalResrcDeps, 'Depends on resources')
+
+ oForm.addMultilineText(TestCaseDataEx.ksParam_sComment, oData.sComment, 'Comment');
+
+ oForm.addSubmit();
+
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py
new file mode 100755
index 00000000..8954afe9
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadmintestgroup.py $
+
+"""
+Test Manager WUI - Test Groups.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml;
+from testmanager.core.db import isDbTimestampInfinity;
+from testmanager.core.testgroup import TestGroupData, TestGroupDataEx;
+from testmanager.core.testcase import TestCaseData, TestCaseLogic;
+
+
+class WuiTestGroup(WuiFormContentBase):
+ """
+ WUI test group content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ assert isinstance(oData, TestGroupDataEx);
+
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add Test Group';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Modify Test Group';
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Test Group';
+ WuiFormContentBase.__init__(self, oData, sMode, 'TestGroup', oDisp, sTitle);
+
+ #
+ # Fetch additional data.
+ #
+ if sMode in [WuiFormContentBase.ksMode_Add, WuiFormContentBase.ksMode_Edit]:
+ self.aoAllTestCases = TestCaseLogic(oDisp.getDb()).fetchForListing(0, 0x7fff, None);
+ else:
+ self.aoAllTestCases = [oMember.oTestCase for oMember in oData.aoMembers];
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO (TestGroupData.ksParam_idTestGroup, self._oData.idTestGroup, 'Test Group ID')
+ oForm.addTimestampRO (TestGroupData.ksParam_tsEffective, self._oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO (TestGroupData.ksParam_tsExpire, self._oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (TestGroupData.ksParam_uidAuthor, self._oData.uidAuthor, 'Changed by UID')
+ oForm.addText (TestGroupData.ksParam_sName, self._oData.sName, 'Name')
+ oForm.addText (TestGroupData.ksParam_sDescription, self._oData.sDescription, 'Description')
+
+ oForm.addListOfTestGroupMembers(TestGroupDataEx.ksParam_aoMembers,
+ oData.aoMembers, self.aoAllTestCases, 'Test Case List',
+ fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show);
+
+ oForm.addMultilineText (TestGroupData.ksParam_sComment, self._oData.sComment, 'Comment');
+ oForm.addSubmit();
+ return True;
+
+
+class WuiTestGroupList(WuiListContentBase):
+ """
+ WUI test group list content generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ assert not aoEntries or isinstance(aoEntries[0], TestGroupDataEx)
+
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, sTitle = 'Test Groups',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = [ 'ID', 'Name', 'Description', 'Test Cases', 'Note', 'Actions' ];
+ self._asColumnAttribs = [ 'align="right"', '', '', '', 'align="center"', 'align="center"' ];
+
+
+ def _formatListEntry(self, iEntry):
+ oEntry = self._aoEntries[iEntry];
+ from testmanager.webui.wuiadmin import WuiAdmin;
+
+ #
+ # Test case list.
+ #
+ sHtml = '';
+ if oEntry.aoMembers:
+ for oMember in oEntry.aoMembers:
+ sHtml += '<dl>\n' \
+ ' <dd><strong>%s</strong> (priority: %d) %s %s</dd>\n' \
+ % ( webutils.escapeElem(oMember.oTestCase.sName),
+ oMember.iSchedPriority,
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idGenTestCase: oMember.oTestCase.idGenTestCase, } ).toHtml(),
+ WuiTmLink('Edit', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseEdit,
+ TestCaseData.ksParam_idTestCase: oMember.oTestCase.idTestCase, } ).toHtml()
+ if isDbTimestampInfinity(oMember.oTestCase.tsExpire)
+ and self._oDisp is not None
+ and not self._oDisp.isReadOnlyUser() else '',
+ );
+
+ sHtml += ' <dt>\n';
+
+ fNoGang = True;
+ for oVar in oMember.oTestCase.aoTestCaseArgs:
+ if oVar.cGangMembers > 1:
+ fNoGang = False
+ break;
+
+ sHtml += ' <table class="tminnertbl" width="100%">\n'
+ if fNoGang:
+ sHtml += ' <tr><th>Timeout</th><th>Arguments</th></tr>\n';
+ else:
+ sHtml += ' <tr><th>Gang Size</th><th>Timeout</th><th style="text-align:left;">Arguments</th></tr>\n';
+
+ cArgsIncluded = 0;
+ for oVar in oMember.oTestCase.aoTestCaseArgs:
+ if oMember.aidTestCaseArgs is None or oVar.idTestCaseArgs in oMember.aidTestCaseArgs:
+ cArgsIncluded += 1;
+ if fNoGang:
+ sHtml += ' <tr>';
+ else:
+ sHtml += ' <tr><td>%s</td>' % (oVar.cGangMembers,);
+ sHtml += '<td>%s</td><td>%s</td></tr>\n' \
+ % ( utils.formatIntervalSeconds(oMember.oTestCase.cSecTimeout if oVar.cSecTimeout is None
+ else oVar.cSecTimeout),
+ webutils.escapeElem(oVar.sArgs), );
+ if cArgsIncluded == 0:
+ sHtml += ' <tr><td colspan="%u">No arguments selected.</td></tr>\n' % ( 2 if fNoGang else 3, );
+ sHtml += ' </table>\n' \
+ ' </dl>\n';
+ oTestCases = WuiRawHtml(sHtml);
+
+ #
+ # Actions.
+ #
+ aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDetails,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }) ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupEdit,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup }));
+ aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupClone,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }));
+ aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDoRemove,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup },
+ sConfirm = 'Do you really want to remove test group #%d?' % (oEntry.idTestGroup,)));
+ else:
+ aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupClone,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }));
+
+
+
+ return [ oEntry.idTestGroup,
+ oEntry.sName,
+ oEntry.sDescription if oEntry.sDescription is not None else '',
+ oTestCases,
+ self._formatCommentCell(oEntry.sComment, cMaxLines = max(3, len(oEntry.aoMembers) * 2)),
+ aoActions ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py
new file mode 100755
index 00000000..30d78cd4
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminuseraccount.py $
+
+"""
+Test Manager WUI - User accounts.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink;
+from testmanager.core.useraccount import UserAccountData
+
+
+class WuiUserAccount(WuiFormContentBase):
+ """
+ WUI user account content generator.
+ """
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add User Account';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Modify User Account';
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'User Account';
+ WuiFormContentBase.__init__(self, oData, sMode, 'User', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO( UserAccountData.ksParam_uid, oData.uid, 'User ID');
+ oForm.addTimestampRO(UserAccountData.ksParam_tsEffective, oData.tsEffective, 'Effective Date');
+ oForm.addTimestampRO(UserAccountData.ksParam_tsExpire, oData.tsExpire, 'Effective Date');
+ oForm.addIntRO( UserAccountData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID');
+ oForm.addText( UserAccountData.ksParam_sLoginName, oData.sLoginName, 'Login name')
+ oForm.addText( UserAccountData.ksParam_sUsername, oData.sUsername, 'User name')
+ oForm.addText( UserAccountData.ksParam_sFullName, oData.sFullName, 'Full name')
+ oForm.addText( UserAccountData.ksParam_sEmail, oData.sEmail, 'E-mail')
+ oForm.addCheckBox( UserAccountData.ksParam_fReadOnly, oData.fReadOnly, 'Only read access')
+ if self._sMode != WuiFormContentBase.ksMode_Show:
+ oForm.addSubmit('Add User' if self._sMode == WuiFormContentBase.ksMode_Add else 'Change User');
+ return True;
+
+
+class WuiUserAccountList(WuiListContentBase):
+ """
+ WUI user account list content generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Registered User Accounts', sId = 'users', fnDPrint = fnDPrint, oDisp = oDisp,
+ aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = ['User ID', 'Name', 'E-mail', 'Full Name', 'Login Name', 'Access', 'Actions'];
+ self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', ];
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ oEntry = self._aoEntries[iEntry];
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserDetails,
+ UserAccountData.ksParam_uid: oEntry.uid } ),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserEdit,
+ UserAccountData.ksParam_uid: oEntry.uid } ),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserDelPost,
+ UserAccountData.ksParam_uid: oEntry.uid },
+ sConfirm = 'Are you sure you want to remove user #%d?' % (oEntry.uid,)),
+ ];
+
+ return [ oEntry.uid, oEntry.sUsername, oEntry.sEmail, oEntry.sFullName, oEntry.sLoginName,
+ 'read only' if oEntry.fReadOnly else 'full',
+ aoActions, ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuibase.py b/src/VBox/ValidationKit/testmanager/webui/wuibase.py
new file mode 100755
index 00000000..feccf20a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuibase.py
@@ -0,0 +1,1245 @@
+# -*- coding: utf-8 -*-
+# $Id: wuibase.py $
+
+"""
+Test Manager Web-UI - Base Classes.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os;
+import sys;
+import string;
+
+# Validation Kit imports.
+from common import webutils, utils;
+from testmanager import config;
+from testmanager.core.base import ModelDataBase, ModelLogicBase, TMExceptionBase;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.systemlog import SystemLogLogic, SystemLogData;
+from testmanager.core.useraccount import UserAccountLogic
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ unicode = str; # pylint: disable=redefined-builtin,invalid-name
+ long = int; # pylint: disable=redefined-builtin,invalid-name
+
+
+class WuiException(TMExceptionBase):
+ """
+ For exceptions raised by Web UI code.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class WuiDispatcherBase(object):
+ """
+ Base class for the Web User Interface (WUI) dispatchers.
+
+ The dispatcher class defines the basics of the page (like base template,
+ menu items, action). It is also responsible for parsing requests and
+ dispatching them to action (POST) or/and content generators (GET+POST).
+ The content returned by the generator is merged into the template and sent
+ back to the webserver glue.
+ """
+
+ ## @todo possible that this should all go into presentation.
+
+ ## The action parameter.
+ ksParamAction = 'Action';
+ ## The name of the default action.
+ ksActionDefault = 'default';
+
+ ## The name of the current page number parameter used when displaying lists.
+ ksParamPageNo = 'PageNo';
+ ## The name of the page length (list items) parameter when displaying lists.
+ ksParamItemsPerPage = 'ItemsPerPage';
+
+ ## The name of the effective date (timestamp) parameter.
+ ksParamEffectiveDate = 'EffectiveDate';
+
+ ## The name of the redirect-to (test manager relative url) parameter.
+ ksParamRedirectTo = 'RedirectTo';
+
+ ## The name of the list-action parameter (WuiListContentWithActionBase).
+ ksParamListAction = 'ListAction';
+
+ ## One or more columns to sort by.
+ ksParamSortColumns = 'SortBy';
+
+ ## The name of the change log enabled/disabled parameter.
+ ksParamChangeLogEnabled = 'ChangeLogEnabled';
+ ## The name of the parmaeter indicating the change log page number.
+ ksParamChangeLogPageNo = 'ChangeLogPageNo';
+ ## The name of the parameter indicate number of change log entries per page.
+ ksParamChangeLogEntriesPerPage = 'ChangeLogEntriesPerPage';
+ ## The change log related parameters.
+ kasChangeLogParams = (ksParamChangeLogEnabled, ksParamChangeLogPageNo, ksParamChangeLogEntriesPerPage,);
+
+ ## @name Dispatcher debugging parameters.
+ ## {@
+ ksParamDbgSqlTrace = 'DbgSqlTrace';
+ ksParamDbgSqlExplain = 'DbgSqlExplain';
+ ## List of all debugging parameters.
+ kasDbgParams = (ksParamDbgSqlTrace, ksParamDbgSqlExplain,);
+ ## @}
+
+ ## Special action return code for skipping _generatePage. Useful for
+ # download pages and the like that messes with the HTTP header and more.
+ ksDispatchRcAllDone = 'Done - Page has been rendered already';
+
+
+ def __init__(self, oSrvGlue, sScriptName):
+ self._oSrvGlue = oSrvGlue;
+ self._oDb = TMDatabaseConnection(self.dprint if config.g_kfWebUiSqlDebug else None, oSrvGlue = oSrvGlue);
+ self._tsNow = None; # Set by getEffectiveDateParam.
+ self._asCheckedParams = [];
+ self._dParams = None; # Set by dispatchRequest.
+ self._sAction = None; # Set by dispatchRequest.
+ self._dDispatch = { self.ksActionDefault: self._actionDefault, };
+
+ # Template bits.
+ self._sTemplate = 'template-default.html';
+ self._sPageTitle = '$$TODO$$'; # The page title.
+ self._aaoMenus = []; # List of [sName, sLink, [ [sSideName, sLink], .. ] tuples.
+ self._sPageFilter = ''; # The filter controls (optional).
+ self._sPageBody = '$$TODO$$'; # The body text.
+ self._dSideMenuFormAttrs = {}; # key/value with attributes for the side menu <form> tag.
+ self._sRedirectTo = None;
+ self._sDebug = '';
+
+ # Debugger bits.
+ self._fDbgSqlTrace = False;
+ self._fDbgSqlExplain = False;
+ self._dDbgParams = {};
+ for sKey, sValue in oSrvGlue.getParameters().items():
+ if sKey in self.kasDbgParams:
+ self._dDbgParams[sKey] = sValue;
+ if self._dDbgParams:
+ from testmanager.webui.wuicontentbase import WuiTmLink;
+ WuiTmLink.kdDbgParams = self._dDbgParams;
+
+ # Determine currently logged in user credentials
+ self._oCurUser = UserAccountLogic(self._oDb).tryFetchAccountByLoginName(oSrvGlue.getLoginName());
+
+ # Calc a couple of URL base strings for this dispatcher.
+ self._sUrlBase = sScriptName + '?';
+ if self._dDbgParams:
+ self._sUrlBase += webutils.encodeUrlParams(self._dDbgParams) + '&';
+ self._sActionUrlBase = self._sUrlBase + self.ksParamAction + '=';
+
+
+ def _redirectPage(self):
+ """
+ Redirects the page to the URL given in self._sRedirectTo.
+ """
+ assert self._sRedirectTo;
+ assert self._sPageBody is None;
+ assert self._sPageTitle is None;
+
+ self._oSrvGlue.setRedirect(self._sRedirectTo);
+ return True;
+
+ def _isMenuMatch(self, sMenuUrl, sActionParam):
+ """ Overridable menu matcher. """
+ return sMenuUrl is not None and sMenuUrl.find(sActionParam) > 0;
+
+ def _isSideMenuMatch(self, sSideMenuUrl, sActionParam):
+ """ Overridable side menu matcher. """
+ return sSideMenuUrl is not None and sSideMenuUrl.find(sActionParam) > 0;
+
+ def _generateMenus(self):
+ """
+ Generates the two menus, returning them as (sTopMenuItems, sSideMenuItems).
+ """
+ fReadOnly = self.isReadOnlyUser();
+
+ #
+ # We use the action to locate the side menu.
+ #
+ aasSideMenu = None;
+ for cchAction in range(len(self._sAction), 1, -1):
+ sActionParam = '%s=%s' % (self.ksParamAction, self._sAction[:cchAction]);
+ for aoItem in self._aaoMenus:
+ if self._isMenuMatch(aoItem[1], sActionParam):
+ aasSideMenu = aoItem[2];
+ break;
+ for asSubItem in aoItem[2]:
+ if self._isMenuMatch(asSubItem[1], sActionParam):
+ aasSideMenu = aoItem[2];
+ break;
+ if aasSideMenu is not None:
+ break;
+
+ #
+ # Top menu first.
+ #
+ sTopMenuItems = '';
+ for aoItem in self._aaoMenus:
+ if aasSideMenu is aoItem[2]:
+ sTopMenuItems += '<li class="current_page_item">';
+ else:
+ sTopMenuItems += '<li>';
+ sTopMenuItems += '<a href="' + webutils.escapeAttr(aoItem[1]) + '">' \
+ + webutils.escapeElem(aoItem[0]) + '</a></li>\n';
+
+ #
+ # Side menu (if found).
+ #
+ sActionParam = '%s=%s' % (self.ksParamAction, self._sAction);
+ sSideMenuItems = '';
+ if aasSideMenu is not None:
+ for asSubItem in aasSideMenu:
+ if asSubItem[1] is not None:
+ if not asSubItem[2] or not fReadOnly:
+ if self._isSideMenuMatch(asSubItem[1], sActionParam):
+ sSideMenuItems += '<li class="current_page_item">';
+ else:
+ sSideMenuItems += '<li>';
+ sSideMenuItems += '<a href="' + webutils.escapeAttr(asSubItem[1]) + '">' \
+ + webutils.escapeElem(asSubItem[0]) + '</a></li>\n';
+ else:
+ sSideMenuItems += '<li class="subheader_item">' + webutils.escapeElem(asSubItem[0]) + '</li>';
+ return (sTopMenuItems, sSideMenuItems);
+
+ def _generatePage(self):
+ """
+ Generates the page using _sTemplate, _sPageTitle, _aaoMenus, and _sPageBody.
+ """
+ assert self._sRedirectTo is None;
+
+ #
+ # Build the replacement string dictionary.
+ #
+
+ # Provide basic auth log out for browsers that supports it.
+ sUserAgent = self._oSrvGlue.getUserAgent();
+ if sUserAgent.startswith('Mozilla/') and sUserAgent.find('Firefox') > 0:
+ # Log in as the logout user in the same realm, the browser forgets
+ # the old login and the job is done. (see apache sample conf)
+ sLogOut = ' (<a href="%s://logout:logout@%s%slogout.py">logout</a>)' \
+ % (self._oSrvGlue.getUrlScheme(), self._oSrvGlue.getUrlNetLoc(), self._oSrvGlue.getUrlBasePath());
+ elif sUserAgent.startswith('Mozilla/') and sUserAgent.find('Safari') > 0:
+ # For a 401, causing the browser to forget the old login. Works
+ # with safari as well as the two above. Since safari consider the
+ # above method a phishing attempt and displays a warning to that
+ # effect, which when taken seriously aborts the logout, this method
+ # is preferable, even if it throws logon boxes in the user's face
+ # till he/she/it hits escape, because it always works.
+ sLogOut = ' (<a href="logout2.py">logout</a>)'
+ elif (sUserAgent.startswith('Mozilla/') and sUserAgent.find('MSIE') > 0) \
+ or (sUserAgent.startswith('Mozilla/') and sUserAgent.find('Chrome') > 0):
+ ## There doesn't seem to be any way to make IE really log out
+ # without using a cookie and systematically 401 accesses based on
+ # some logout state associated with it. Not sure how secure that
+ # can be made and we really want to avoid cookies. So, perhaps,
+ # just avoid IE for now. :-)
+ ## Chrome/21.0 doesn't want to log out either.
+ sLogOut = ''
+ else:
+ sLogOut = ''
+
+ # Prep Menus.
+ (sTopMenuItems, sSideMenuItems) = self._generateMenus();
+
+ # The dictionary (max variable length is 28 chars (see further down)).
+ dReplacements = {
+ '@@PAGE_TITLE@@': self._sPageTitle,
+ '@@LOG_OUT@@': sLogOut,
+ '@@TESTMANAGER_VERSION@@': config.g_ksVersion,
+ '@@TESTMANAGER_REVISION@@': config.g_ksRevision,
+ '@@BASE_URL@@': self._oSrvGlue.getBaseUrl(),
+ '@@TOP_MENU_ITEMS@@': sTopMenuItems,
+ '@@SIDE_MENU_ITEMS@@': sSideMenuItems,
+ '@@SIDE_FILTER_CONTROL@@': self._sPageFilter,
+ '@@SIDE_MENU_FORM_ATTRS@@': '',
+ '@@PAGE_BODY@@': self._sPageBody,
+ '@@DEBUG@@': '',
+ };
+
+ # Side menu form attributes.
+ if self._dSideMenuFormAttrs:
+ dReplacements['@@SIDE_MENU_FORM_ATTRS@@'] = ' '.join(['%s="%s"' % (sKey, webutils.escapeAttr(sValue))
+ for sKey, sValue in self._dSideMenuFormAttrs.items()]);
+
+ # Special current user handling.
+ if self._oCurUser is not None:
+ dReplacements['@@USER_NAME@@'] = self._oCurUser.sUsername;
+ else:
+ dReplacements['@@USER_NAME@@'] = 'unauthorized user "' + self._oSrvGlue.getLoginName() + '"';
+
+ # Prep debug section.
+ if self._sDebug == '':
+ if config.g_kfWebUiSqlTrace or self._fDbgSqlTrace or self._fDbgSqlExplain:
+ self._sDebug = '<h3>Processed in %s ns.</h3>\n%s\n' \
+ % ( utils.formatNumber(utils.timestampNano() - self._oSrvGlue.tsStart,),
+ self._oDb.debugHtmlReport(self._oSrvGlue.tsStart));
+ elif config.g_kfWebUiProcessedIn:
+ self._sDebug = '<h3>Processed in %s ns.</h3>\n' \
+ % ( utils.formatNumber(utils.timestampNano() - self._oSrvGlue.tsStart,), );
+ if config.g_kfWebUiDebugPanel:
+ self._sDebug += self._debugRenderPanel();
+ if self._sDebug != '':
+ dReplacements['@@DEBUG@@'] = u'<div id="debug"><br><br><hr/>' \
+ + (utils.toUnicode(self._sDebug, errors='ignore') if isinstance(self._sDebug, str)
+ else self._sDebug) \
+ + u'</div>\n';
+
+ #
+ # Load the template.
+ #
+ with open(os.path.join(self._oSrvGlue.pathTmWebUI(), self._sTemplate)) as oFile: # pylint: disable=unspecified-encoding
+ sTmpl = oFile.read();
+
+ #
+ # Process the template, outputting each part we process.
+ #
+ offStart = 0;
+ offCur = 0;
+ while offCur < len(sTmpl):
+ # Look for a replacement variable.
+ offAtAt = sTmpl.find('@@', offCur);
+ if offAtAt < 0:
+ break;
+ offCur = offAtAt + 2;
+ if sTmpl[offCur] not in string.ascii_uppercase:
+ continue;
+ offEnd = sTmpl.find('@@', offCur, offCur+28);
+ if offEnd <= 0:
+ continue;
+ offCur = offEnd;
+ sReplacement = sTmpl[offAtAt:offEnd+2];
+ if sReplacement in dReplacements:
+ # Got a match! Write out the previous chunk followed by the replacement text.
+ if offStart < offAtAt:
+ self._oSrvGlue.write(sTmpl[offStart:offAtAt]);
+ self._oSrvGlue.write(dReplacements[sReplacement]);
+ # Advance past the replacement point in the template.
+ offCur += 2;
+ offStart = offCur;
+ else:
+ assert False, 'Unknown replacement "%s" at offset %s in %s' % (sReplacement, offAtAt, self._sTemplate );
+
+ # The final chunk.
+ if offStart < len(sTmpl):
+ self._oSrvGlue.write(sTmpl[offStart:]);
+
+ return True;
+
+ #
+ # Interface for WuiContentBase classes.
+ #
+
+ def getUrlNoParams(self):
+ """
+ Returns the base URL without any parameters (no trailing '?' or &).
+ """
+ return self._sUrlBase[:self._sUrlBase.rindex('?')];
+
+ def getUrlBase(self):
+ """
+ Returns the base URL, ending with '?' or '&'.
+ This may already include some debug parameters.
+ """
+ return self._sUrlBase;
+
+ def getParameters(self):
+ """
+ Returns a (shallow) copy of the request parameter dictionary.
+ """
+ return self._dParams.copy();
+
+ def getDb(self):
+ """
+ Returns the database connection.
+ """
+ return self._oDb;
+
+ def getNow(self):
+ """
+ Returns the effective date.
+ """
+ return self._tsNow;
+
+
+ #
+ # Parameter handling.
+ #
+
+ def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False):
+ """
+ Gets a string parameter.
+ Raises exception if not found and sDefault is None.
+ """
+ if sName in self._dParams:
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+ sValue = self._dParams[sName];
+ if isinstance(sValue, list):
+ raise WuiException('%s parameter "%s" is given multiple times: "%s"' % (self._sAction, sName, sValue));
+ sValue = sValue.strip();
+ elif sDefault is None and fAllowNull is not True:
+ raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName,));
+ else:
+ sValue = sDefault;
+
+ if asValidValues is not None and sValue not in asValidValues:
+ raise WuiException('%s parameter %s value "%s" not in %s '
+ % (self._sAction, sName, sValue, asValidValues));
+ return sValue;
+
+ def getBoolParam(self, sName, fDefault = None):
+ """
+ Gets a boolean parameter.
+ Raises exception if not found and fDefault is None, or if not a valid boolean.
+ """
+ sValue = self.getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'],
+ '0' if fDefault is None else str(fDefault));
+ # HACK: Checkboxes doesn't return a value when unchecked, so we always
+ # provide a default when dealing with boolean parameters.
+ return sValue in ('True', 'true', '1',);
+
+ def getIntParam(self, sName, iMin = None, iMax = None, iDefault = None):
+ """
+ Gets a integer parameter.
+ Raises exception if not found and iDefault is None, if not a valid int,
+ or if outside the range defined by iMin and iMax.
+ """
+ if iDefault is not None and sName not in self._dParams:
+ return iDefault;
+
+ sValue = self.getStringParam(sName, None, None if iDefault is None else str(iDefault));
+ try:
+ iValue = int(sValue);
+ except:
+ raise WuiException('%s parameter %s value "%s" cannot be convert to an integer'
+ % (self._sAction, sName, sValue));
+
+ if (iMin is not None and iValue < iMin) \
+ or (iMax is not None and iValue > iMax):
+ raise WuiException('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sAction, sName, iValue, iMin, iMax));
+ return iValue;
+
+ def getLongParam(self, sName, lMin = None, lMax = None, lDefault = None):
+ """
+ Gets a long integer parameter.
+ Raises exception if not found and lDefault is None, if not a valid long,
+ or if outside the range defined by lMin and lMax.
+ """
+ if lDefault is not None and sName not in self._dParams:
+ return lDefault;
+
+ sValue = self.getStringParam(sName, None, None if lDefault is None else str(lDefault));
+ try:
+ lValue = long(sValue);
+ except:
+ raise WuiException('%s parameter %s value "%s" cannot be convert to an integer'
+ % (self._sAction, sName, sValue));
+
+ if (lMin is not None and lValue < lMin) \
+ or (lMax is not None and lValue > lMax):
+ raise WuiException('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sAction, sName, lValue, lMin, lMax));
+ return lValue;
+
+ def getTsParam(self, sName, tsDefault = None, fRequired = True):
+ """
+ Gets a timestamp parameter.
+ Raises exception if not found and fRequired is True.
+ """
+ if fRequired is False and sName not in self._dParams:
+ return tsDefault;
+
+ sValue = self.getStringParam(sName, None, None if tsDefault is None else str(tsDefault));
+ (sValue, sError) = ModelDataBase.validateTs(sValue);
+ if sError is not None:
+ raise WuiException('%s parameter %s value "%s": %s'
+ % (self._sAction, sName, sValue, sError));
+ return sValue;
+
+ def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None):
+ """
+ Gets parameter list.
+ Raises exception if not found and aiDefaults is None, or if any of the
+ values are not valid integers or outside the range defined by iMin and iMax.
+ """
+ if sName in self._dParams:
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ if isinstance(self._dParams[sName], list):
+ asValues = self._dParams[sName];
+ else:
+ asValues = [self._dParams[sName],];
+ aiValues = [];
+ for sValue in asValues:
+ try:
+ iValue = int(sValue);
+ except:
+ raise WuiException('%s parameter %s value "%s" cannot be convert to an integer'
+ % (self._sAction, sName, sValue));
+
+ if (iMin is not None and iValue < iMin) \
+ or (iMax is not None and iValue > iMax):
+ raise WuiException('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sAction, sName, iValue, iMin, iMax));
+ aiValues.append(iValue);
+ else:
+ aiValues = aiDefaults;
+
+ return aiValues;
+
+ def getListOfStrParams(self, sName, asDefaults = None):
+ """
+ Gets parameter list.
+ Raises exception if not found and asDefaults is None.
+ """
+ if sName in self._dParams:
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ if isinstance(self._dParams[sName], list):
+ asValues = [str(s).strip() for s in self._dParams[sName]];
+ else:
+ asValues = [str(self._dParams[sName]).strip(), ];
+ elif asDefaults is None:
+ raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName,));
+ else:
+ asValues = asDefaults;
+
+ return asValues;
+
+ def getListOfTestCasesParam(self, sName, asDefaults = None): # too many local vars - pylint: disable=too-many-locals
+ """Get list of test cases and their parameters"""
+ if sName in self._dParams:
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName)
+
+ aoListOfTestCases = []
+
+ aiSelectedTestCaseIds = self.getListOfIntParams('%s[asCheckedTestCases]' % sName, aiDefaults=[])
+ aiAllTestCases = self.getListOfIntParams('%s[asAllTestCases]' % sName, aiDefaults=[])
+
+ for idTestCase in aiAllTestCases:
+ aiCheckedTestCaseArgs = \
+ self.getListOfIntParams(
+ '%s[%d][asCheckedTestCaseArgs]' % (sName, idTestCase),
+ aiDefaults=[])
+
+ aiAllTestCaseArgs = \
+ self.getListOfIntParams(
+ '%s[%d][asAllTestCaseArgs]' % (sName, idTestCase),
+ aiDefaults=[])
+
+ oListEntryTestCaseArgs = []
+ for idTestCaseArgs in aiAllTestCaseArgs:
+ fArgsChecked = idTestCaseArgs in aiCheckedTestCaseArgs;
+
+ # Dry run
+ sPrefix = '%s[%d][%d]' % (sName, idTestCase, idTestCaseArgs,);
+ self.getIntParam(sPrefix + '[idTestCaseArgs]', iDefault = -1,)
+
+ sArgs = self.getStringParam(sPrefix + '[sArgs]', sDefault = '')
+ cSecTimeout = self.getStringParam(sPrefix + '[cSecTimeout]', sDefault = '')
+ cGangMembers = self.getStringParam(sPrefix + '[cGangMembers]', sDefault = '')
+ cGangMembers = self.getStringParam(sPrefix + '[cGangMembers]', sDefault = '')
+
+ oListEntryTestCaseArgs.append((fArgsChecked, idTestCaseArgs, sArgs, cSecTimeout, cGangMembers))
+
+ sTestCaseName = self.getStringParam('%s[%d][sName]' % (sName, idTestCase), sDefault='')
+
+ oListEntryTestCase = (
+ idTestCase,
+ idTestCase in aiSelectedTestCaseIds,
+ sTestCaseName,
+ oListEntryTestCaseArgs
+ );
+
+ aoListOfTestCases.append(oListEntryTestCase)
+
+ if not aoListOfTestCases:
+ if asDefaults is None:
+ raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName))
+ aoListOfTestCases = asDefaults
+
+ return aoListOfTestCases
+
+ def getEffectiveDateParam(self, sParamName = None):
+ """
+ Gets the effective date parameter.
+
+ Returns a timestamp suitable for database and url parameters.
+ Returns None if not found or empty.
+
+ The first call with sParamName set to None will set the internal _tsNow
+ value upon successfull return.
+ """
+
+ sName = sParamName if sParamName is not None else WuiDispatcherBase.ksParamEffectiveDate
+
+ if sName not in self._dParams:
+ return None;
+
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ sValue = self._dParams[sName];
+ if isinstance(sValue, list):
+ raise WuiException('%s parameter "%s" is given multiple times: %s' % (self._sAction, sName, sValue));
+ sValue = sValue.strip();
+ if sValue == '':
+ return None;
+
+ #
+ # Timestamp, just validate it and return.
+ #
+ if sValue[0] not in ['-', '+']:
+ (sValue, sError) = ModelDataBase.validateTs(sValue);
+ if sError is not None:
+ raise WuiException('%s parameter "%s" ("%s") is invalid: %s' % (self._sAction, sName, sValue, sError));
+ if sParamName is None and self._tsNow is None:
+ self._tsNow = sValue;
+ return sValue;
+
+ #
+ # Relative timestamp. Validate and convert it to a fixed timestamp.
+ #
+ chSign = sValue[0];
+ (sValue, sError) = ModelDataBase.validateTs(sValue[1:], fRelative = True);
+ if sError is not None:
+ raise WuiException('%s parameter "%s" ("%s") is invalid: %s' % (self._sAction, sName, sValue, sError));
+ if sValue[-6] in ['-', '+']:
+ raise WuiException('%s parameter "%s" ("%s") is a relative timestamp but incorrectly includes a time zone.'
+ % (self._sAction, sName, sValue));
+ offTime = 11;
+ if sValue[offTime - 1] != ' ':
+ raise WuiException('%s parameter "%s" ("%s") incorrect format.' % (self._sAction, sName, sValue));
+ sInterval = 'P' + sValue[:(offTime - 1)] + 'T' + sValue[offTime:];
+
+ self._oDb.execute('SELECT CURRENT_TIMESTAMP ' + chSign + ' \'' + sInterval + '\'::INTERVAL');
+ oDate = self._oDb.fetchOne()[0];
+
+ sValue = str(oDate);
+ if sParamName is None and self._tsNow is None:
+ self._tsNow = sValue;
+ return sValue;
+
+ def getRedirectToParameter(self, sDefault = None):
+ """
+ Gets the special redirect to parameter if it exists, will Return default
+ if not, with None being a valid default.
+
+ Makes sure the it doesn't got offsite.
+ Raises exception if invalid.
+ """
+ if sDefault is not None or self.ksParamRedirectTo in self._dParams:
+ sValue = self.getStringParam(self.ksParamRedirectTo, sDefault = sDefault);
+ cch = sValue.find("?");
+ if cch < 0:
+ cch = sValue.find("#");
+ if cch < 0:
+ cch = len(sValue);
+ for ch in (':', '/', '\\', '..'):
+ if sValue.find(ch, 0, cch) >= 0:
+ raise WuiException('Invalid character (%c) in redirect-to url: %s' % (ch, sValue,));
+ else:
+ sValue = None;
+ return sValue;
+
+
+ def _checkForUnknownParameters(self):
+ """
+ Check if we've handled all parameters, raises exception if anything
+ unknown was found.
+ """
+
+ if len(self._asCheckedParams) != len(self._dParams):
+ sUnknownParams = '';
+ for sKey in self._dParams:
+ if sKey not in self._asCheckedParams:
+ sUnknownParams += ' ' + sKey + '=' + str(self._dParams[sKey]);
+ raise WuiException('Unknown parameters: ' + sUnknownParams);
+
+ return True;
+
+ def _assertPostRequest(self):
+ """
+ Makes sure that the request we're dispatching is a POST request.
+ Raises an exception of not.
+ """
+ if self._oSrvGlue.getMethod() != 'POST':
+ raise WuiException('Expected "POST" request, got "%s"' % (self._oSrvGlue.getMethod(),))
+ return True;
+
+ #
+ # Client browser type.
+ #
+
+ ## @name Browser types.
+ ## @{
+ ksBrowserFamily_Unknown = 0;
+ ksBrowserFamily_Gecko = 1;
+ ksBrowserFamily_Webkit = 2;
+ ksBrowserFamily_Trident = 3;
+ ## @}
+
+ ## @name Browser types.
+ ## @{
+ ksBrowserType_FamilyMask = 0xff;
+ ksBrowserType_Unknown = 0;
+ ksBrowserType_Firefox = (1 << 8) | ksBrowserFamily_Gecko;
+ ksBrowserType_Chrome = (2 << 8) | ksBrowserFamily_Webkit;
+ ksBrowserType_Safari = (3 << 8) | ksBrowserFamily_Webkit;
+ ksBrowserType_IE = (4 << 8) | ksBrowserFamily_Trident;
+ ## @}
+
+ def getBrowserType(self):
+ """
+ Gets the browser type.
+ The browser family can be extracted from this using ksBrowserType_FamilyMask.
+ """
+ sAgent = self._oSrvGlue.getUserAgent();
+ if sAgent.find('AppleWebKit/') > 0:
+ if sAgent.find('Chrome/') > 0:
+ return self.ksBrowserType_Chrome;
+ if sAgent.find('Safari/') > 0:
+ return self.ksBrowserType_Safari;
+ return self.ksBrowserType_Unknown | self.ksBrowserFamily_Webkit;
+ if sAgent.find('Gecko/') > 0:
+ if sAgent.find('Firefox/') > 0:
+ return self.ksBrowserType_Firefox;
+ return self.ksBrowserType_Unknown | self.ksBrowserFamily_Gecko;
+ return self.ksBrowserType_Unknown | self.ksBrowserFamily_Unknown;
+
+ def isBrowserGecko(self, sMinVersion = None):
+ """ Returns true if it's a gecko based browser. """
+ if (self.getBrowserType() & self.ksBrowserType_FamilyMask) != self.ksBrowserFamily_Gecko:
+ return False;
+ if sMinVersion is not None:
+ sAgent = self._oSrvGlue.getUserAgent();
+ sVersion = sAgent[sAgent.find('Gecko/')+6:].split()[0];
+ if sVersion < sMinVersion:
+ return False;
+ return True;
+
+ #
+ # User related stuff.
+ #
+
+ def isReadOnlyUser(self):
+ """ Returns true if the logged in user is read-only or if no user is logged in. """
+ return self._oCurUser is None or self._oCurUser.fReadOnly;
+
+
+ #
+ # Debugging
+ #
+
+ def _debugProcessDispatch(self):
+ """
+ Processes any debugging parameters in the request and adds them to
+ _asCheckedParams so they won't cause trouble in the action handler.
+ """
+
+ self._fDbgSqlTrace = self.getBoolParam(self.ksParamDbgSqlTrace, False);
+ self._fDbgSqlExplain = self.getBoolParam(self.ksParamDbgSqlExplain, False);
+
+ if self._fDbgSqlExplain:
+ self._oDb.debugEnableExplain();
+
+ return True;
+
+ def _debugRenderPanel(self):
+ """
+ Renders a simple form for controlling WUI debugging.
+
+ Returns the HTML for it.
+ """
+
+ sHtml = '<div id="debug-panel">\n' \
+ ' <form id="debug-panel-form" method="get" action="#">\n';
+
+ for sKey, oValue in self._dParams.items():
+ if sKey not in self.kasDbgParams:
+ if hasattr(oValue, 'startswith'):
+ sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oValue),);
+ else:
+ for oSubValue in oValue:
+ sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oSubValue),);
+
+ for aoCheckBox in (
+ [self.ksParamDbgSqlTrace, self._fDbgSqlTrace, 'SQL trace'],
+ [self.ksParamDbgSqlExplain, self._fDbgSqlExplain, 'SQL explain'], ):
+ sHtml += ' <input type="checkbox" name="%s" value="1"%s />%s\n' \
+ % (aoCheckBox[0], ' checked' if aoCheckBox[1] else '', aoCheckBox[2]);
+
+ sHtml += ' <button type="submit">Apply</button>\n';
+ sHtml += ' </form>\n' \
+ '</div>\n';
+ return sHtml;
+
+
+ def _debugGetParameters(self):
+ """
+ Gets a dictionary with the debug parameters.
+
+ For use when links are constructed from scratch instead of self._dParams.
+ """
+ return self._dDbgParams;
+
+ #
+ # Dispatching
+ #
+
+ def _actionDefault(self):
+ """The default action handler, always overridden. """
+ raise WuiException('The child class shall override WuiBase.actionDefault().')
+
+ def _actionGenericListing(self, oLogicType, oListContentType):
+ """
+ Generic listing action.
+
+ oLogicType implements fetchForListing.
+ oListContentType is a child of WuiListContentBase.
+ """
+ tsEffective = self.getEffectiveDateParam();
+ cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 384);
+ iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0);
+ aiSortColumnsDup = self.getListOfIntParams(self.ksParamSortColumns,
+ iMin = -getattr(oLogicType, 'kcMaxSortColumns', 0) + 1,
+ iMax = getattr(oLogicType, 'kcMaxSortColumns', 0), aiDefaults = []);
+ aiSortColumns = [];
+ for iSortColumn in aiSortColumnsDup:
+ if iSortColumn not in aiSortColumns:
+ aiSortColumns.append(iSortColumn);
+ self._checkForUnknownParameters();
+
+ ## @todo fetchForListing could be made more useful if it returned a tuple
+ # that includes the total number of entries, thus making paging more user
+ # friendly (known number of pages). So, the return should be:
+ # (aoEntries, cAvailableEntries)
+ #
+ # In addition, we could add a new parameter to include deleted entries,
+ # making it easier to find old deleted testboxes/testcases/whatever and
+ # clone them back to life. The temporal navigation is pretty usless here.
+ #
+ aoEntries = oLogicType(self._oDb).fetchForListing(iPage * cItemsPerPage, cItemsPerPage + 1, tsEffective, aiSortColumns);
+ oContent = oListContentType(aoEntries, iPage, cItemsPerPage, tsEffective,
+ fnDPrint = self._oSrvGlue.dprint, oDisp = self, aiSelectedSortColumns = aiSortColumns);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
+ def _actionGenericFormAdd(self, oDataType, oFormType, sRedirectTo = None):
+ """
+ Generic add something form display request handler.
+
+ oDataType is a ModelDataBase child class.
+ oFormType is a WuiFormContentBase child class.
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ oData = oDataType().initFromParams(oDisp = self, fStrict = False);
+ sRedirectTo = self.getRedirectToParameter(sRedirectTo);
+ self._checkForUnknownParameters();
+
+ oForm = oFormType(oData, oFormType.ksMode_Add, oDisp = self);
+ oForm.setRedirectTo(sRedirectTo);
+ (self._sPageTitle, self._sPageBody) = oForm.showForm();
+ return True
+
+ def _actionGenericFormDetails(self, oDataType, oLogicType, oFormType, sIdAttr = None, sGenIdAttr = None): # pylint: disable=too-many-locals
+ """
+ Generic handler for showing a details form/page.
+
+ oDataType is a ModelDataBase child class.
+ oLogicType may implement fetchForChangeLog.
+ oFormType is a WuiFormContentBase child class.
+ sIdParamName is the name of the ID parameter (not idGen!).
+ """
+ # Input.
+ assert issubclass(oDataType, ModelDataBase);
+ assert issubclass(oLogicType, ModelLogicBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ if sIdAttr is None:
+ sIdAttr = oDataType.ksIdAttr;
+ if sGenIdAttr is None:
+ sGenIdAttr = getattr(oDataType, 'ksGenIdAttr', None);
+
+ # Parameters.
+ idGenObject = -1;
+ if sGenIdAttr is not None:
+ idGenObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sGenIdAttr), 0, 0x7ffffffe, -1);
+ if idGenObject != -1:
+ idObject = tsNow = None;
+ else:
+ idObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sIdAttr), 0, 0x7ffffffe, -1);
+ tsNow = self.getEffectiveDateParam();
+ fChangeLog = self.getBoolParam(WuiDispatcherBase.ksParamChangeLogEnabled, True);
+ iChangeLogPageNo = self.getIntParam(WuiDispatcherBase.ksParamChangeLogPageNo, 0, 9999, 0);
+ cChangeLogEntriesPerPage = self.getIntParam(WuiDispatcherBase.ksParamChangeLogEntriesPerPage, 2, 9999, 4);
+ self._checkForUnknownParameters();
+
+ # Fetch item and display it.
+ if idGenObject == -1:
+ oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow);
+ else:
+ oData = oDataType().initFromDbWithGenId(self._oDb, idGenObject);
+
+ oContent = oFormType(oData, oFormType.ksMode_Show, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.showForm();
+
+ # Add change log if supported.
+ if fChangeLog and hasattr(oLogicType, 'fetchForChangeLog'):
+ (aoEntries, fMore) = oLogicType(self._oDb).fetchForChangeLog(getattr(oData, sIdAttr),
+ iChangeLogPageNo * cChangeLogEntriesPerPage,
+ cChangeLogEntriesPerPage ,
+ tsNow);
+ self._sPageBody += oContent.showChangeLog(aoEntries, fMore, iChangeLogPageNo, cChangeLogEntriesPerPage, tsNow);
+ return True
+
+ def _actionGenericDoRemove(self, oLogicType, sParamId, sRedirAction):
+ """
+ Delete entry (using oLogicType.removeEntry).
+
+ oLogicType is a class that implements addEntry.
+
+ sParamId is the name (ksParam_...) of the HTTP variable hold the ID of
+ the database entry to delete.
+
+ sRedirAction is what action to redirect to on success.
+ """
+ import cgitb;
+
+ idEntry = self.getIntParam(sParamId, iMin = 1, iMax = 0x7ffffffe)
+ fCascade = self.getBoolParam('fCascadeDelete', False);
+ sRedirectTo = self.getRedirectToParameter(self._sActionUrlBase + sRedirAction);
+ self._checkForUnknownParameters()
+
+ try:
+ if self.isReadOnlyUser():
+ raise Exception('"%s" is a read only user!' % (self._oCurUser.sUsername,));
+ self._sPageTitle = None
+ self._sPageBody = None
+ self._sRedirectTo = sRedirectTo;
+ return oLogicType(self._oDb).removeEntry(self._oCurUser.uid, idEntry, fCascade = fCascade, fCommit = True);
+ except Exception as oXcpt:
+ self._oDb.rollback();
+ self._sPageTitle = 'Unable to delete entry';
+ self._sPageBody = str(oXcpt);
+ if config.g_kfDebugDbXcpt:
+ self._sPageBody += cgitb.html(sys.exc_info());
+ self._sRedirectTo = None;
+ return False;
+
+ def _actionGenericFormEdit(self, oDataType, oFormType, sIdParamName = None, sRedirectTo = None):
+ """
+ Generic edit something form display request handler.
+
+ oDataType is a ModelDataBase child class.
+ oFormType is a WuiFormContentBase child class.
+ sIdParamName is the name of the ID parameter (not idGen!).
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ if sIdParamName is None:
+ sIdParamName = getattr(oDataType, 'ksParam_' + oDataType.ksIdAttr);
+ assert len(sIdParamName) > 1;
+
+ tsNow = self.getEffectiveDateParam();
+ idObject = self.getIntParam(sIdParamName, 0, 0x7ffffffe);
+ sRedirectTo = self.getRedirectToParameter(sRedirectTo);
+ self._checkForUnknownParameters();
+ oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow = tsNow);
+
+ oContent = oFormType(oData, oFormType.ksMode_Edit, oDisp = self);
+ oContent.setRedirectTo(sRedirectTo);
+ (self._sPageTitle, self._sPageBody) = oContent.showForm();
+ return True
+
+ def _actionGenericFormEditL(self, oCoreObjectLogic, sCoreObjectIdFieldName, oWuiObjectLogic):
+ """
+ Generic modify something form display request handler.
+
+ @param oCoreObjectLogic A *Logic class
+
+ @param sCoreObjectIdFieldName Name of HTTP POST variable that
+ contains object ID information
+
+ @param oWuiObjectLogic Web interface renderer class
+ """
+
+ iCoreDataObjectId = self.getIntParam(sCoreObjectIdFieldName, 0, 0x7ffffffe, -1)
+ self._checkForUnknownParameters();
+
+ ## @todo r=bird: This will return a None object if the object wasn't found... Crash bang in the content generator
+ # code (that's not logic code btw.).
+ oData = oCoreObjectLogic(self._oDb).getById(iCoreDataObjectId)
+
+ # Instantiate and render the MODIFY dialog form
+ oContent = oWuiObjectLogic(oData, oWuiObjectLogic.ksMode_Edit, oDisp=self)
+
+ (self._sPageTitle, self._sPageBody) = oContent.showForm()
+
+ return True
+
+ def _actionGenericFormClone(self, oDataType, oFormType, sIdAttr, sGenIdAttr = None):
+ """
+ Generic clone something form display request handler.
+
+ oDataType is a ModelDataBase child class.
+ oFormType is a WuiFormContentBase child class.
+ sIdParamName is the name of the ID parameter.
+ sGenIdParamName is the name of the generation ID parameter, None if not applicable.
+ """
+ # Input.
+ assert issubclass(oDataType, ModelDataBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ # Parameters.
+ idGenObject = -1;
+ if sGenIdAttr is not None:
+ idGenObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sGenIdAttr), 0, 0x7ffffffe, -1);
+ if idGenObject != -1:
+ idObject = tsNow = None;
+ else:
+ idObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sIdAttr), 0, 0x7ffffffe, -1);
+ tsNow = self.getEffectiveDateParam();
+ self._checkForUnknownParameters();
+
+ # Fetch data and clear identifying attributes not relevant to the clone.
+ if idGenObject != -1:
+ oData = oDataType().initFromDbWithGenId(self._oDb, idGenObject);
+ else:
+ oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow);
+
+ setattr(oData, sIdAttr, None);
+ if sGenIdAttr is not None:
+ setattr(oData, sGenIdAttr, None);
+ oData.tsEffective = None;
+ oData.tsExpire = None;
+
+ # Display form.
+ oContent = oFormType(oData, oFormType.ksMode_Add, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.showForm()
+ return True
+
+
+ def _actionGenericFormPost(self, sMode, fnLogicAction, oDataType, oFormType, sRedirectTo, fStrict = True):
+ """
+ Generic POST request handling from a WuiFormContentBase child.
+
+ oDataType is a ModelDataBase child class.
+ oFormType is a WuiFormContentBase child class.
+ fnLogicAction is a method taking a oDataType instance and uidAuthor as arguments.
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ #
+ # Read and validate parameters.
+ #
+ oData = oDataType().initFromParams(oDisp = self, fStrict = fStrict);
+ sRedirectTo = self.getRedirectToParameter(sRedirectTo);
+ self._checkForUnknownParameters();
+ self._assertPostRequest();
+ if sMode == WuiFormContentBase.ksMode_Add and getattr(oData, 'kfIdAttrIsForForeign', False):
+ enmValidateFor = oData.ksValidateFor_AddForeignId;
+ elif sMode == WuiFormContentBase.ksMode_Add:
+ enmValidateFor = oData.ksValidateFor_Add;
+ else:
+ enmValidateFor = oData.ksValidateFor_Edit;
+ dErrors = oData.validateAndConvert(self._oDb, enmValidateFor);
+
+ # Check that the user can do this.
+ sErrorMsg = None;
+ assert self._oCurUser is not None;
+ if self.isReadOnlyUser():
+ sErrorMsg = 'User %s is not allowed to modify anything!' % (self._oCurUser.sUsername,)
+
+ if not dErrors and not sErrorMsg:
+ oData.convertFromParamNull();
+
+ #
+ # Try do the job.
+ #
+ try:
+ fnLogicAction(oData, self._oCurUser.uid, fCommit = True);
+ except Exception as oXcpt:
+ self._oDb.rollback();
+ oForm = oFormType(oData, sMode, oDisp = self);
+ oForm.setRedirectTo(sRedirectTo);
+ sErrorMsg = str(oXcpt) if not config.g_kfDebugDbXcpt else '\n'.join(utils.getXcptInfo(4));
+ (self._sPageTitle, self._sPageBody) = oForm.showForm(sErrorMsg = sErrorMsg);
+ else:
+ #
+ # Worked, redirect to the specified page.
+ #
+ self._sPageTitle = None;
+ self._sPageBody = None;
+ self._sRedirectTo = sRedirectTo;
+ else:
+ oForm = oFormType(oData, sMode, oDisp = self);
+ oForm.setRedirectTo(sRedirectTo);
+ (self._sPageTitle, self._sPageBody) = oForm.showForm(dErrors = dErrors, sErrorMsg = sErrorMsg);
+ return True;
+
+ def _actionGenericFormAddPost(self, oDataType, oLogicType, oFormType, sRedirAction, fStrict=True):
+ """
+ Generic add entry POST request handling from a WuiFormContentBase child.
+
+ oDataType is a ModelDataBase child class.
+ oLogicType is a class that implements addEntry.
+ oFormType is a WuiFormContentBase child class.
+ sRedirAction is what action to redirect to on success.
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ assert issubclass(oLogicType, ModelLogicBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ oLogic = oLogicType(self._oDb);
+ return self._actionGenericFormPost(WuiFormContentBase.ksMode_Add, oLogic.addEntry, oDataType, oFormType,
+ '?' + webutils.encodeUrlParams({self.ksParamAction: sRedirAction}), fStrict=fStrict)
+
+ def _actionGenericFormEditPost(self, oDataType, oLogicType, oFormType, sRedirAction, fStrict = True):
+ """
+ Generic edit POST request handling from a WuiFormContentBase child.
+
+ oDataType is a ModelDataBase child class.
+ oLogicType is a class that implements addEntry.
+ oFormType is a WuiFormContentBase child class.
+ sRedirAction is what action to redirect to on success.
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ assert issubclass(oLogicType, ModelLogicBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ oLogic = oLogicType(self._oDb);
+ return self._actionGenericFormPost(WuiFormContentBase.ksMode_Edit, oLogic.editEntry, oDataType, oFormType,
+ '?' + webutils.encodeUrlParams({self.ksParamAction: sRedirAction}),
+ fStrict = fStrict);
+
+ def _unauthorizedUser(self):
+ """
+ Displays the unauthorized user message (corresponding record is not
+ present in DB).
+ """
+
+ sLoginName = self._oSrvGlue.getLoginName();
+
+ # Report to system log
+ oSystemLogLogic = SystemLogLogic(self._oDb);
+ oSystemLogLogic.addEntry(SystemLogData.ksEvent_UserAccountUnknown,
+ 'Unknown user (%s) attempts to access from %s'
+ % (sLoginName, self._oSrvGlue.getClientAddr()),
+ 24, fCommit = True)
+
+ # Display message.
+ self._sPageTitle = 'User not authorized'
+ self._sPageBody = """
+ <p>Access denied for user <b>%s</b>.
+ Please contact an admin user to set up your access.</p>
+ """ % (sLoginName,)
+ return True;
+
+ def dispatchRequest(self):
+ """
+ Dispatches a request.
+ """
+
+ #
+ # Get the parameters and checks for duplicates.
+ #
+ try:
+ dParams = self._oSrvGlue.getParameters();
+ except Exception as oXcpt:
+ raise WuiException('Error retriving parameters: %s' % (oXcpt,));
+
+ for sKey in dParams.keys():
+
+ # Take care about strings which may contain unicode characters: convert percent-encoded symbols back to unicode.
+ for idxItem, _ in enumerate(dParams[sKey]):
+ dParams[sKey][idxItem] = utils.toUnicode(dParams[sKey][idxItem], 'utf-8');
+
+ if not len(dParams[sKey]) > 1:
+ dParams[sKey] = dParams[sKey][0];
+ self._dParams = dParams;
+
+ #
+ # Figure out the requested action and validate it.
+ #
+ if self.ksParamAction in self._dParams:
+ self._sAction = self._dParams[self.ksParamAction];
+ self._asCheckedParams.append(self.ksParamAction);
+ else:
+ self._sAction = self.ksActionDefault;
+
+ if isinstance(self._sAction, list) or self._sAction not in self._dDispatch:
+ raise WuiException('Unknown action "%s" requested' % (self._sAction,));
+
+ #
+ # Call action handler and generate the page (if necessary).
+ #
+ if self._oCurUser is not None:
+ self._debugProcessDispatch();
+ if self._dDispatch[self._sAction]() is self.ksDispatchRcAllDone:
+ return True;
+ else:
+ self._unauthorizedUser();
+
+ if self._sRedirectTo is None:
+ self._generatePage();
+ else:
+ self._redirectPage();
+ return True;
+
+
+ def dprint(self, sText):
+ """ Debug printing. """
+ if config.g_kfWebUiDebug:
+ self._oSrvGlue.dprint(sText);
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py
new file mode 100755
index 00000000..c91e6036
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py
@@ -0,0 +1,1290 @@
+# -*- coding: utf-8 -*-
+# $Id: wuicontentbase.py $
+
+"""
+Test Manager Web-UI - Content Base Classes.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import copy;
+import sys;
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager import config;
+from testmanager.webui.wuibase import WuiDispatcherBase, WuiException;
+from testmanager.webui.wuihlpform import WuiHlpForm;
+from testmanager.core import db;
+from testmanager.core.base import AttributeChangeEntryPre;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ unicode = str; # pylint: disable=redefined-builtin,invalid-name
+
+
+class WuiHtmlBase(object): # pylint: disable=too-few-public-methods
+ """
+ Base class for HTML objects.
+ """
+
+ def __init__(self):
+ """Dummy init to shut up pylint."""
+ pass; # pylint: disable=unnecessary-pass
+
+ def toHtml(self):
+
+ """
+ Must be overridden by sub-classes.
+ """
+ assert False;
+ return '';
+
+ def __str__(self):
+ """ String representation is HTML, simplifying formatting and such. """
+ return self.toHtml();
+
+
+class WuiLinkBase(WuiHtmlBase): # pylint: disable=too-few-public-methods
+ """
+ For passing links from WuiListContentBase._formatListEntry.
+ """
+
+ def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None,
+ sFragmentId = None, fBracketed = True, sExtraAttrs = ''):
+ WuiHtmlBase.__init__(self);
+ self.sName = sName
+ self.sUrl = sUrlBase
+ self.sConfirm = sConfirm;
+ self.sTitle = sTitle;
+ self.fBracketed = fBracketed;
+ self.sExtraAttrs = sExtraAttrs;
+
+ if dParams:
+ # Do some massaging of None arguments.
+ dParams = dict(dParams);
+ for sKey in dParams:
+ if dParams[sKey] is None:
+ dParams[sKey] = '';
+ self.sUrl += '?' + webutils.encodeUrlParams(dParams);
+
+ if sFragmentId is not None:
+ self.sUrl += '#' + sFragmentId;
+
+ def setBracketed(self, fBracketed):
+ """Changes the bracketing style."""
+ self.fBracketed = fBracketed;
+ return True;
+
+ def toHtml(self):
+ """
+ Returns a simple HTML anchor element.
+ """
+ sExtraAttrs = self.sExtraAttrs;
+ if self.sConfirm is not None:
+ sExtraAttrs += 'onclick=\'return confirm("%s");\' ' % (webutils.escapeAttr(self.sConfirm),);
+ if self.sTitle is not None:
+ sExtraAttrs += 'title="%s" ' % (webutils.escapeAttr(self.sTitle),);
+ if sExtraAttrs and sExtraAttrs[-1] != ' ':
+ sExtraAttrs += ' ';
+
+ sFmt = '[<a %shref="%s">%s</a>]';
+ if not self.fBracketed:
+ sFmt = '<a %shref="%s">%s</a>';
+ return sFmt % (sExtraAttrs, webutils.escapeAttr(self.sUrl), webutils.escapeElem(self.sName));
+
+ @staticmethod
+ def estimateStringWidth(sString):
+ """
+ Takes a string and estimate it's width so the caller can pad with
+ U+2002 before tab in a title text. This is very very rough.
+ """
+ cchWidth = 0;
+ for ch in sString:
+ if ch.isupper() or ch in u'wm\u2007\u2003\u2001\u3000':
+ cchWidth += 2;
+ else:
+ cchWidth += 1;
+ return cchWidth;
+
+ @staticmethod
+ def getStringWidthPaddingEx(cchWidth, cchMaxWidth):
+ """ Works with estiamteStringWidth(). """
+ if cchWidth + 2 <= cchMaxWidth:
+ return u'\u2002' * ((cchMaxWidth - cchWidth) * 2 // 3)
+ return u'';
+
+ @staticmethod
+ def getStringWidthPadding(sString, cchMaxWidth):
+ """ Works with estiamteStringWidth(). """
+ return WuiLinkBase.getStringWidthPaddingEx(WuiLinkBase.estimateStringWidth(sString), cchMaxWidth);
+
+ @staticmethod
+ def padStringToWidth(sString, cchMaxWidth):
+ """ Works with estimateStringWidth. """
+ cchWidth = WuiLinkBase.estimateStringWidth(sString);
+ if cchWidth < cchMaxWidth:
+ return sString + WuiLinkBase.getStringWidthPaddingEx(cchWidth, cchMaxWidth);
+ return sString;
+
+
+class WuiTmLink(WuiLinkBase): # pylint: disable=too-few-public-methods
+ """ Local link to the test manager. """
+
+ kdDbgParams = [];
+
+ def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None,
+ sFragmentId = None, fBracketed = True):
+
+ # Add debug parameters if necessary.
+ if self.kdDbgParams:
+ if not dParams:
+ dParams = dict(self.kdDbgParams);
+ else:
+ dParams = dict(dParams);
+ for sKey in self.kdDbgParams:
+ if sKey not in dParams:
+ dParams[sKey] = self.kdDbgParams[sKey];
+
+ WuiLinkBase.__init__(self, sName, sUrlBase, dParams, sConfirm, sTitle, sFragmentId, fBracketed);
+
+
+class WuiAdminLink(WuiTmLink): # pylint: disable=too-few-public-methods
+ """ Local link to the test manager's admin portion. """
+
+ def __init__(self, sName, sAction, tsEffectiveDate = None, dParams = None, sConfirm = None, sTitle = None,
+ sFragmentId = None, fBracketed = True):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ if not dParams:
+ dParams = {};
+ else:
+ dParams = dict(dParams);
+ if sAction is not None:
+ dParams[WuiAdmin.ksParamAction] = sAction;
+ if tsEffectiveDate is not None:
+ dParams[WuiAdmin.ksParamEffectiveDate] = tsEffectiveDate;
+ WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
+ sFragmentId = sFragmentId, fBracketed = fBracketed);
+
+class WuiMainLink(WuiTmLink): # pylint: disable=too-few-public-methods
+ """ Local link to the test manager's main portion. """
+
+ def __init__(self, sName, sAction, dParams = None, sConfirm = None, sTitle = None, sFragmentId = None, fBracketed = True):
+ if not dParams:
+ dParams = {};
+ else:
+ dParams = dict(dParams);
+ from testmanager.webui.wuimain import WuiMain;
+ if sAction is not None:
+ dParams[WuiMain.ksParamAction] = sAction;
+ WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
+ sFragmentId = sFragmentId, fBracketed = fBracketed);
+
+class WuiSvnLink(WuiLinkBase): # pylint: disable=too-few-public-methods
+ """
+ For linking to a SVN revision.
+ """
+ def __init__(self, iRevision, sName = None, fBracketed = True, sExtraAttrs = ''):
+ if sName is None:
+ sName = 'r%s' % (iRevision,);
+ WuiLinkBase.__init__(self, sName, config.g_ksTracLogUrlPrefix, { 'rev': iRevision,},
+ fBracketed = fBracketed, sExtraAttrs = sExtraAttrs);
+
+class WuiSvnLinkWithTooltip(WuiSvnLink): # pylint: disable=too-few-public-methods
+ """
+ For linking to a SVN revision with changelog tooltip.
+ """
+ def __init__(self, iRevision, sRepository, sName = None, fBracketed = True):
+ sExtraAttrs = ' onmouseover="return svnHistoryTooltipShow(event,\'%s\',%s);" onmouseout="return tooltipHide();"' \
+ % ( sRepository, iRevision, );
+ WuiSvnLink.__init__(self, iRevision, sName = sName, fBracketed = fBracketed, sExtraAttrs = sExtraAttrs);
+
+class WuiBuildLogLink(WuiLinkBase):
+ """
+ For linking to a build log.
+ """
+ def __init__(self, sUrl, sName = None, fBracketed = True):
+ assert sUrl;
+ if sName is None:
+ sName = 'Build log';
+ if not webutils.hasSchema(sUrl):
+ WuiLinkBase.__init__(self, sName, config.g_ksBuildLogUrlPrefix + sUrl, fBracketed = fBracketed);
+ else:
+ WuiLinkBase.__init__(self, sName, sUrl, fBracketed = fBracketed);
+
+class WuiRawHtml(WuiHtmlBase): # pylint: disable=too-few-public-methods
+ """
+ For passing raw html from WuiListContentBase._formatListEntry.
+ """
+ def __init__(self, sHtml):
+ self.sHtml = sHtml;
+ WuiHtmlBase.__init__(self);
+
+ def toHtml(self):
+ return self.sHtml;
+
+class WuiHtmlKeeper(WuiHtmlBase): # pylint: disable=too-few-public-methods
+ """
+ For keeping a list of elements, concatenating their toHtml output together.
+ """
+ def __init__(self, aoInitial = None, sSep = ' '):
+ WuiHtmlBase.__init__(self);
+ self.sSep = sSep;
+ self.aoKept = [];
+ if aoInitial is not None:
+ if isinstance(aoInitial, WuiHtmlBase):
+ self.aoKept.append(aoInitial);
+ else:
+ self.aoKept.extend(aoInitial);
+
+ def append(self, oObject):
+ """ Appends one objects. """
+ self.aoKept.append(oObject);
+
+ def extend(self, aoObjects):
+ """ Appends a list of objects. """
+ self.aoKept.extend(aoObjects);
+
+ def toHtml(self):
+ return self.sSep.join(oObj.toHtml() for oObj in self.aoKept);
+
+class WuiSpanText(WuiRawHtml): # pylint: disable=too-few-public-methods
+ """
+ Outputs the given text within a span of the given CSS class.
+ """
+ def __init__(self, sSpanClass, sText, sTitle = None):
+ if sTitle is None:
+ WuiRawHtml.__init__(self,
+ u'<span class="%s">%s</span>'
+ % ( webutils.escapeAttr(sSpanClass), webutils.escapeElem(sText),));
+ else:
+ WuiRawHtml.__init__(self,
+ u'<span class="%s" title="%s">%s</span>'
+ % ( webutils.escapeAttr(sSpanClass), webutils.escapeAttr(sTitle), webutils.escapeElem(sText),));
+
+class WuiElementText(WuiRawHtml): # pylint: disable=too-few-public-methods
+ """
+ Outputs the given element text.
+ """
+ def __init__(self, sText):
+ WuiRawHtml.__init__(self, webutils.escapeElem(sText));
+
+
+class WuiContentBase(object): # pylint: disable=too-few-public-methods
+ """
+ Base for the content classes.
+ """
+
+ ## The text/symbol for a very short add link.
+ ksShortAddLink = u'\u2795'
+ ## HTML hex entity string for ksShortAddLink.
+ ksShortAddLinkHtml = '&#x2795;;'
+ ## The text/symbol for a very short edit link.
+ ksShortEditLink = u'\u270D'
+ ## HTML hex entity string for ksShortDetailsLink.
+ ksShortEditLinkHtml = '&#x270d;'
+ ## The text/symbol for a very short details link.
+ ksShortDetailsLink = u'\U0001f6c8\ufe0e'
+ ## HTML hex entity string for ksShortDetailsLink.
+ ksShortDetailsLinkHtml = '&#x1f6c8;;&#xfe0e;'
+ ## The text/symbol for a very short change log / details / previous page link.
+ ksShortChangeLogLink = u'\u2397'
+ ## HTML hex entity string for ksShortDetailsLink.
+ ksShortChangeLogLinkHtml = '&#x2397;'
+ ## The text/symbol for a very short reports link.
+ ksShortReportLink = u'\U0001f4ca\ufe0e'
+ ## HTML hex entity string for ksShortReportLink.
+ ksShortReportLinkHtml = '&#x1f4ca;&#xfe0e;'
+ ## The text/symbol for a very short test results link.
+ ksShortTestResultsLink = u'\U0001f5d0\ufe0e'
+
+
+ def __init__(self, fnDPrint = None, oDisp = None):
+ self._oDisp = oDisp; # WuiDispatcherBase.
+ self._fnDPrint = fnDPrint;
+ if fnDPrint is None and oDisp is not None:
+ self._fnDPrint = oDisp.dprint;
+
+ def dprint(self, sText):
+ """ Debug printing. """
+ if self._fnDPrint:
+ self._fnDPrint(sText);
+
+ @staticmethod
+ def formatTsShort(oTs):
+ """
+ Formats a timestamp (db rep) into a short form.
+ """
+ oTsZulu = db.dbTimestampToZuluDatetime(oTs);
+ sTs = oTsZulu.strftime('%Y-%m-%d %H:%M:%SZ');
+ return unicode(sTs).replace('-', u'\u2011').replace(' ', u'\u00a0');
+
+ def getNowTs(self):
+ """ Gets a database compatible current timestamp from python. See db.dbTimestampPythonNow(). """
+ return db.dbTimestampPythonNow();
+
+ def formatIntervalShort(self, oInterval):
+ """
+ Formats an interval (db rep) into a short form.
+ """
+ # default formatting for negative intervals.
+ if oInterval.days < 0:
+ return str(oInterval);
+
+ # Figure the hour, min and sec counts.
+ cHours = oInterval.seconds // 3600;
+ cMinutes = (oInterval.seconds % 3600) // 60;
+ cSeconds = oInterval.seconds - cHours * 3600 - cMinutes * 60;
+
+ # Tailor formatting to the interval length.
+ if oInterval.days > 0:
+ if oInterval.days > 1:
+ return '%d days, %d:%02d:%02d' % (oInterval.days, cHours, cMinutes, cSeconds);
+ return '1 day, %d:%02d:%02d' % (cHours, cMinutes, cSeconds);
+ if cMinutes > 0 or cSeconds >= 30 or cHours > 0:
+ return '%d:%02d:%02d' % (cHours, cMinutes, cSeconds);
+ if cSeconds >= 10:
+ return '%d.%ds' % (cSeconds, oInterval.microseconds // 100000);
+ if cSeconds > 0:
+ return '%d.%02ds' % (cSeconds, oInterval.microseconds // 10000);
+ return '%d ms' % (oInterval.microseconds // 1000,);
+
+ @staticmethod
+ def genericPageWalker(iCurItem, cItems, sHrefFmt, cWidth = 11, iBase = 1, sItemName = 'page'):
+ """
+ Generic page walker generator.
+
+ sHrefFmt has three %s sequences:
+ 1. The first is the page number link parameter (0-based).
+ 2. The title text, iBase-based number or text.
+ 3. The link text, iBase-based number or text.
+ """
+
+ # Calc display range.
+ iStart = 0 if iCurItem - cWidth // 2 <= cWidth // 4 else iCurItem - cWidth // 2;
+ iEnd = iStart + cWidth;
+ if iEnd > cItems:
+ iEnd = cItems;
+ if cItems > cWidth:
+ iStart = cItems - cWidth;
+
+ sHtml = u'';
+
+ # Previous page (using << >> because &laquo; and &raquo are too tiny).
+ if iCurItem > 0:
+ sHtml += '%s&nbsp;&nbsp;' % sHrefFmt % (iCurItem - 1, 'previous ' + sItemName, '&lt;&lt;');
+ else:
+ sHtml += '&lt;&lt;&nbsp;&nbsp;';
+
+ # 1 2 3 4...
+ if iStart > 0:
+ sHtml += '%s&nbsp; ... &nbsp;\n' % (sHrefFmt % (0, 'first %s' % (sItemName,), 0 + iBase),);
+
+ sHtml += '&nbsp;\n'.join(sHrefFmt % (i, '%s %d' % (sItemName, i + iBase), i + iBase) if i != iCurItem
+ else unicode(i + iBase)
+ for i in range(iStart, iEnd));
+ if iEnd < cItems:
+ sHtml += '&nbsp; ... &nbsp;%s\n' % (sHrefFmt % (cItems - 1, 'last %s' % (sItemName,), cItems - 1 + iBase));
+
+ # Next page.
+ if iCurItem + 1 < cItems:
+ sHtml += '&nbsp;&nbsp;%s' % sHrefFmt % (iCurItem + 1, 'next ' + sItemName, '&gt;&gt;');
+ else:
+ sHtml += '&nbsp;&nbsp;&gt;&gt;';
+
+ return sHtml;
+
+class WuiSingleContentBase(WuiContentBase): # pylint: disable=too-few-public-methods
+ """
+ Base for the content classes working on a single data object (oData).
+ """
+ def __init__(self, oData, oDisp = None, fnDPrint = None):
+ WuiContentBase.__init__(self, oDisp = oDisp, fnDPrint = fnDPrint);
+ self._oData = oData; # Usually ModelDataBase.
+
+
+class WuiFormContentBase(WuiSingleContentBase): # pylint: disable=too-few-public-methods
+ """
+ Base class for simple input form content classes (single data object).
+ """
+
+ ## @name Form mode.
+ ## @{
+ ksMode_Add = 'add';
+ ksMode_Edit = 'edit';
+ ksMode_Show = 'show';
+ ## @}
+
+ ## Default action mappings.
+ kdSubmitActionMappings = {
+ ksMode_Add: 'AddPost',
+ ksMode_Edit: 'EditPost',
+ };
+
+ def __init__(self, oData, sMode, sCoreName, oDisp, sTitle, sId = None, fEditable = True, sSubmitAction = None):
+ WuiSingleContentBase.__init__(self, copy.copy(oData), oDisp);
+ assert sMode in [self.ksMode_Add, self.ksMode_Edit, self.ksMode_Show];
+ assert len(sTitle) > 1;
+ assert sId is None or sId;
+
+ self._sMode = sMode;
+ self._sCoreName = sCoreName;
+ self._sActionBase = 'ksAction' + sCoreName;
+ self._sTitle = sTitle;
+ self._sId = sId if sId is not None else (type(oData).__name__.lower() + 'form');
+ self._fEditable = fEditable and (oDisp is None or not oDisp.isReadOnlyUser())
+ self._sSubmitAction = sSubmitAction;
+ if sSubmitAction is None and sMode != self.ksMode_Show:
+ self._sSubmitAction = getattr(oDisp, self._sActionBase + self.kdSubmitActionMappings[sMode]);
+ self._sRedirectTo = None;
+
+
+ def _populateForm(self, oForm, oData):
+ """
+ Populates the form. oData has parameter NULL values.
+ This must be reimplemented by the child.
+ """
+ _ = oForm; _ = oData;
+ raise Exception('Reimplement me!');
+
+ def _generatePostFormContent(self, oData):
+ """
+ Generate optional content that comes below the form.
+ Returns a list of tuples, where the first tuple element is the title
+ and the second the content. I.e. similar to show() output.
+ """
+ _ = oData;
+ return [];
+
+ @staticmethod
+ def _calcChangeLogEntryLinks(aoEntries, iEntry):
+ """
+ Returns an array of links to go with the change log entry.
+ """
+ _ = aoEntries; _ = iEntry;
+ ## @todo detect deletion and recreation.
+ ## @todo view details link.
+ ## @todo restore link (need new action)
+ ## @todo clone link.
+ return [];
+
+ @staticmethod
+ def _guessChangeLogEntryDescription(aoEntries, iEntry):
+ """
+ Guesses the action + author that caused the change log entry.
+ Returns descriptive string.
+ """
+ oEntry = aoEntries[iEntry];
+
+ # Figure the author of the change.
+ if oEntry.sAuthor is not None:
+ sAuthor = '%s (#%s)' % (oEntry.sAuthor, oEntry.uidAuthor,);
+ elif oEntry.uidAuthor is not None:
+ sAuthor = '#%d (??)' % (oEntry.uidAuthor,);
+ else:
+ sAuthor = None;
+
+ # Figure the action.
+ if oEntry.oOldRaw is None:
+ if sAuthor is None:
+ return 'Created by batch job.';
+ return 'Created by %s.' % (sAuthor,);
+
+ if sAuthor is None:
+ return 'Automatically updated.'
+ return 'Modified by %s.' % (sAuthor,);
+
+ @staticmethod
+ def formatChangeLogEntry(aoEntries, iEntry, sUrl, dParams):
+ """
+ Formats one change log entry into one or more HTML table rows.
+
+ The sUrl and dParams arguments are used to construct links to historical
+ data using the tsEffective value. If no links wanted, they'll both be None.
+
+ Note! The parameters are given as array + index in case someone wishes
+ to access adjacent entries later in order to generate better
+ change descriptions.
+ """
+ oEntry = aoEntries[iEntry];
+
+ # Turn the effective date into a URL if we can:
+ if sUrl:
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = oEntry.tsEffective;
+ sEffective = WuiLinkBase(WuiFormContentBase.formatTsShort(oEntry.tsEffective), sUrl,
+ dParams, fBracketed = False).toHtml();
+ else:
+ sEffective = webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsEffective))
+
+ # The primary row.
+ sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven';
+ sContent = ' <tr class="%s">\n' \
+ ' <td rowspan="%d">%s</td>\n' \
+ ' <td rowspan="%d">%s</td>\n' \
+ ' <td colspan="3">%s%s</td>\n' \
+ ' </tr>\n' \
+ % ( sRowClass,
+ len(oEntry.aoChanges) + 1, sEffective,
+ len(oEntry.aoChanges) + 1, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsExpire)),
+ WuiFormContentBase._guessChangeLogEntryDescription(aoEntries, iEntry),
+ ' '.join(oLink.toHtml() for oLink in WuiFormContentBase._calcChangeLogEntryLinks(aoEntries, iEntry)),);
+
+ # Additional rows for each changed attribute.
+ j = 0;
+ for oChange in oEntry.aoChanges:
+ if isinstance(oChange, AttributeChangeEntryPre):
+ sContent += ' <tr class="%s%s"><td>%s</td>'\
+ '<td><div class="tdpre">%s%s%s</div></td>' \
+ '<td><div class="tdpre">%s%s%s</div></td></tr>\n' \
+ % ( sRowClass, 'odd' if j & 1 else 'even',
+ webutils.escapeElem(oChange.sAttr),
+ '<pre>' if oChange.sOldText else '',
+ webutils.escapeElem(oChange.sOldText),
+ '</pre>' if oChange.sOldText else '',
+ '<pre>' if oChange.sNewText else '',
+ webutils.escapeElem(oChange.sNewText),
+ '</pre>' if oChange.sNewText else '', );
+ else:
+ sContent += ' <tr class="%s%s"><td>%s</td><td>%s</td><td>%s</td></tr>\n' \
+ % ( sRowClass, 'odd' if j & 1 else 'even',
+ webutils.escapeElem(oChange.sAttr),
+ webutils.escapeElem(oChange.sOldText),
+ webutils.escapeElem(oChange.sNewText), );
+ j += 1;
+
+ return sContent;
+
+ def _showChangeLogNavi(self, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, sWhere):
+ """
+ Returns the HTML for the change log navigator.
+ Note! See also _generateNavigation.
+ """
+ sNavigation = '<div class="tmlistnav-%s">\n' % sWhere;
+ sNavigation += ' <table class="tmlistnavtab">\n' \
+ ' <tr>\n';
+ dParams = self._oDisp.getParameters();
+ dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage;
+ dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo;
+ if tsNow is not None:
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = tsNow;
+
+ # Prev and combo box in one cell. Both inside the form for formatting reasons.
+ sNavigation += ' <td>\n' \
+ ' <form name="ChangeLogEntriesPerPageForm" method="GET">\n'
+
+ # Prev
+ if iPageNo > 0:
+ dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo - 1;
+ sNavigation += '<a href="?%s#tmchangelog">Previous</a>\n' \
+ % (webutils.encodeUrlParams(dParams),);
+ dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo;
+ else:
+ sNavigation += 'Previous\n';
+
+ # Entries per page selector.
+ del dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage];
+ sNavigation += '&nbsp; &nbsp;\n' \
+ ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value + \'#tmchangelog\';" ' \
+ 'title="Max change log entries per page">\n' \
+ % (WuiDispatcherBase.ksParamChangeLogEntriesPerPage,
+ webutils.encodeUrlParams(dParams),
+ WuiDispatcherBase.ksParamChangeLogEntriesPerPage);
+ dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage;
+
+ for iEntriesPerPage in [2, 4, 8, 16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 8192]:
+ sNavigation += ' <option value="%d" %s>%d entries per page</option>\n' \
+ % ( iEntriesPerPage,
+ 'selected="selected"' if iEntriesPerPage == cEntriesPerPage else '',
+ iEntriesPerPage );
+ sNavigation += ' </select>\n';
+
+ # End of cell (and form).
+ sNavigation += ' </form>\n' \
+ ' </td>\n';
+
+ # Next
+ if fMoreEntries:
+ dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo + 1;
+ sNavigation += ' <td><a href="?%s#tmchangelog">Next</a></td>\n' \
+ % (webutils.encodeUrlParams(dParams),);
+ else:
+ sNavigation += ' <td>Next</td>\n';
+
+ sNavigation += ' </tr>\n' \
+ ' </table>\n' \
+ '</div>\n';
+ return sNavigation;
+
+ def setRedirectTo(self, sRedirectTo):
+ """
+ For setting the hidden redirect-to field.
+ """
+ self._sRedirectTo = sRedirectTo;
+ return True;
+
+ def showChangeLog(self, aoEntries, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, fShowNavigation = True):
+ """
+ Render the change log, returning raw HTML.
+ aoEntries is an array of ChangeLogEntry.
+ """
+ sContent = '\n' \
+ '<hr>\n' \
+ '<div id="tmchangelog">\n' \
+ ' <h3>Change Log </h3>\n';
+ if fShowNavigation:
+ sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'top');
+ sContent += ' <table class="tmtable tmchangelog">\n' \
+ ' <thead class="tmheader">' \
+ ' <tr>' \
+ ' <th rowspan="2">When</th>\n' \
+ ' <th rowspan="2">Expire (excl)</th>\n' \
+ ' <th colspan="3">Changes</th>\n' \
+ ' </tr>\n' \
+ ' <tr>\n' \
+ ' <th>Attribute</th>\n' \
+ ' <th>Old value</th>\n' \
+ ' <th>New value</th>\n' \
+ ' </tr>\n' \
+ ' </thead>\n' \
+ ' <tbody>\n';
+
+ if self._sMode == self.ksMode_Show:
+ sUrl = self._oDisp.getUrlNoParams();
+ dParams = self._oDisp.getParameters();
+ else:
+ sUrl = None;
+ dParams = None;
+
+ for iEntry, _ in enumerate(aoEntries):
+ sContent += self.formatChangeLogEntry(aoEntries, iEntry, sUrl, dParams);
+
+ sContent += ' </tbody>\n' \
+ ' </table>\n';
+ if fShowNavigation and len(aoEntries) >= 8:
+ sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'bottom');
+ sContent += '</div>\n\n';
+ return sContent;
+
+ def _generateTopRowFormActions(self, oData):
+ """
+ Returns a list of WuiTmLinks.
+ """
+ aoActions = [];
+ if self._sMode == self.ksMode_Show and self._fEditable:
+ # Remove _idGen and effective date since we're always editing the current data,
+ # and make sure the primary ID is present. Also remove change log stuff.
+ dParams = self._oDisp.getParameters();
+ if hasattr(oData, 'ksIdGenAttr'):
+ sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr);
+ if sIdGenParam in dParams:
+ del dParams[sIdGenParam];
+ for sParam in [ WuiDispatcherBase.ksParamEffectiveDate, ] + list(WuiDispatcherBase.kasChangeLogParams):
+ if sParam in dParams:
+ del dParams[sParam];
+ dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
+
+ dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit');
+ aoActions.append(WuiTmLink('Edit', '', dParams));
+
+ # Add clone operation if available. This uses the same data selection as for showing details. No change log.
+ if hasattr(self._oDisp, self._sActionBase + 'Clone'):
+ dParams = self._oDisp.getParameters();
+ for sParam in WuiDispatcherBase.kasChangeLogParams:
+ if sParam in dParams:
+ del dParams[sParam];
+ dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Clone');
+ aoActions.append(WuiTmLink('Clone', '', dParams));
+
+ elif self._sMode == self.ksMode_Edit:
+ # Details views the details at a given time, so we need either idGen or an effecive date + regular id.
+ dParams = {};
+ if hasattr(oData, 'ksIdGenAttr'):
+ sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr);
+ dParams[sIdGenParam] = getattr(oData, oData.ksIdGenAttr);
+ elif hasattr(oData, 'tsEffective'):
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = oData.tsEffective;
+ dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
+ dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details');
+ aoActions.append(WuiTmLink('Details', '', dParams));
+
+ # Add delete operation if available.
+ if hasattr(self._oDisp, self._sActionBase + 'DoRemove'):
+ dParams = self._oDisp.getParameters();
+ dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'DoRemove');
+ dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
+ aoActions.append(WuiTmLink('Delete', '', dParams, sConfirm = "Are you absolutely sure?"));
+
+ return aoActions;
+
+ def showForm(self, dErrors = None, sErrorMsg = None):
+ """
+ Render the form.
+ """
+ oForm = WuiHlpForm(self._sId,
+ '?' + webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sSubmitAction}),
+ dErrors if dErrors is not None else {},
+ fReadOnly = self._sMode == self.ksMode_Show);
+
+ self._oData.convertToParamNull();
+
+ # If form cannot be constructed due to some reason we
+ # need to show this reason
+ try:
+ self._populateForm(oForm, self._oData);
+ if self._sRedirectTo is not None:
+ oForm.addTextHidden(self._oDisp.ksParamRedirectTo, self._sRedirectTo);
+ except WuiException as oXcpt:
+ sContent = unicode(oXcpt)
+ else:
+ sContent = oForm.finalize();
+
+ # Add any post form content.
+ atPostFormContent = self._generatePostFormContent(self._oData);
+ if atPostFormContent:
+ for iSection, tSection in enumerate(atPostFormContent):
+ (sSectionTitle, sSectionContent) = tSection;
+ sContent += u'<div id="postform-%d" class="tmformpostsection">\n' % (iSection,);
+ if sSectionTitle:
+ sContent += '<h3 class="tmformpostheader">%s</h3>\n' % (webutils.escapeElem(sSectionTitle),);
+ sContent += u' <div id="postform-%d-content" class="tmformpostcontent">\n' % (iSection,);
+ sContent += sSectionContent;
+ sContent += u' </div>\n' \
+ u'</div>\n';
+
+ # Add action to the top.
+ aoActions = self._generateTopRowFormActions(self._oData);
+ if aoActions:
+ sActionLinks = '<p>%s</p>' % (' '.join(unicode(oLink) for oLink in aoActions));
+ sContent = sActionLinks + sContent;
+
+ # Add error info to the top.
+ if sErrorMsg is not None:
+ sContent = '<p class="tmerrormsg">' + webutils.escapeElem(sErrorMsg) + '</p>\n' + sContent;
+
+ return (self._sTitle, sContent);
+
+ def getListOfItems(self, asListItems = tuple(), asSelectedItems = tuple()):
+ """
+ Format generic list which should be used by HTML form
+ """
+ aoRet = []
+ for sListItem in asListItems:
+ fEnabled = sListItem in asSelectedItems;
+ aoRet.append((sListItem, fEnabled, sListItem))
+ return aoRet
+
+
+class WuiListContentBase(WuiContentBase):
+ """
+ Base for the list content classes.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments
+ sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None, fTimeNavigation = True):
+ WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
+ self._aoEntries = aoEntries; ## @todo should replace this with a Logic object and define methods for querying.
+ self._iPage = iPage;
+ self._cItemsPerPage = cItemsPerPage;
+ self._tsEffectiveDate = tsEffectiveDate;
+ self._fTimeNavigation = fTimeNavigation;
+ self._sTitle = sTitle; assert len(sTitle) > 1;
+ if sId is None:
+ sId = sTitle.strip().replace(' ', '').lower();
+ assert sId.strip();
+ self._sId = sId;
+ self._asColumnHeaders = [];
+ self._asColumnAttribs = [];
+ self._aaiColumnSorting = []; ##< list of list of integers
+ self._aiSelectedSortColumns = aiSelectedSortColumns; ##< list of integers
+
+ def _formatCommentCell(self, sComment, cMaxLines = 3, cchMaxLine = 63):
+ """
+ Helper functions for formatting comment cell.
+ Returns None or WuiRawHtml instance.
+ """
+ # Nothing to do for empty comments.
+ if sComment is None:
+ return None;
+ sComment = sComment.strip();
+ if not sComment:
+ return None;
+
+ # Restrict the text if necessary, making the whole text available thru mouse-over.
+ ## @todo this would be better done by java script or smth, so it could automatically adjust to the table size.
+ if len(sComment) > cchMaxLine or sComment.count('\n') >= cMaxLines:
+ sShortHtml = '';
+ for iLine, sLine in enumerate(sComment.split('\n')):
+ if iLine >= cMaxLines:
+ break;
+ if iLine > 0:
+ sShortHtml += '<br>\n';
+ if len(sLine) > cchMaxLine:
+ sShortHtml += webutils.escapeElem(sLine[:(cchMaxLine - 3)]);
+ sShortHtml += '...';
+ else:
+ sShortHtml += webutils.escapeElem(sLine);
+ return WuiRawHtml('<span class="tmcomment" title="%s">%s</span>' % (webutils.escapeAttr(sComment), sShortHtml,));
+
+ return WuiRawHtml('<span class="tmcomment">%s</span>' % (webutils.escapeElem(sComment).replace('\n', '<br>'),));
+
+ def _formatListEntry(self, iEntry):
+ """
+ Formats the specified list entry as a list of column values.
+ Returns HTML for a table row.
+
+ The child class really need to override this!
+ """
+ # ASSUMES ModelDataBase children.
+ asRet = [];
+ for sAttr in self._aoEntries[0].getDataAttributes():
+ asRet.append(getattr(self._aoEntries[iEntry], sAttr));
+ return asRet;
+
+ def _formatListEntryHtml(self, iEntry):
+ """
+ Formats the specified list entry as HTML.
+ Returns HTML for a table row.
+
+ The child class can override this to
+ """
+ if (iEntry + 1) & 1:
+ sRow = u' <tr class="tmodd">\n';
+ else:
+ sRow = u' <tr class="tmeven">\n';
+
+ aoValues = self._formatListEntry(iEntry);
+ assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders));
+
+ for i, _ in enumerate(aoValues):
+ if i < len(self._asColumnAttribs) and self._asColumnAttribs[i]:
+ sRow += u' <td ' + self._asColumnAttribs[i] + '>';
+ else:
+ sRow += u' <td>';
+
+ if isinstance(aoValues[i], WuiHtmlBase):
+ sRow += aoValues[i].toHtml();
+ elif isinstance(aoValues[i], list):
+ if aoValues[i]:
+ for oElement in aoValues[i]:
+ if isinstance(oElement, WuiHtmlBase):
+ sRow += oElement.toHtml();
+ elif db.isDbTimestamp(oElement):
+ sRow += webutils.escapeElem(self.formatTsShort(oElement));
+ else:
+ sRow += webutils.escapeElem(unicode(oElement));
+ sRow += ' ';
+ elif db.isDbTimestamp(aoValues[i]):
+ sRow += webutils.escapeElem(self.formatTsShort(aoValues[i]));
+ elif db.isDbInterval(aoValues[i]):
+ sRow += webutils.escapeElem(self.formatIntervalShort(aoValues[i]));
+ elif aoValues[i] is not None:
+ sRow += webutils.escapeElem(unicode(aoValues[i]));
+
+ sRow += u'</td>\n';
+
+ return sRow + u' </tr>\n';
+
+ @staticmethod
+ def generateTimeNavigationComboBox(sWhere, dParams, tsEffective):
+ """
+ Generates the HTML for the xxxx ago combo box form.
+ """
+ sNavigation = '<form name="TmTimeNavCombo-%s" method="GET">\n' % (sWhere,);
+ sNavigation += ' <select name="%s" onchange="window.location=' % (WuiDispatcherBase.ksParamEffectiveDate);
+ sNavigation += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), WuiDispatcherBase.ksParamEffectiveDate)
+ sNavigation += 'this.options[this.selectedIndex].value;" title="Effective date">\n';
+
+ aoWayBackPoints = [
+ ('+0000-00-00 00:00:00.00', 'Now', ' title="Present Day. Present Time."'), # lain :)
+
+ ('-0000-00-00 01:00:00.00', '1 hour ago', ''),
+ ('-0000-00-00 02:00:00.00', '2 hours ago', ''),
+ ('-0000-00-00 03:00:00.00', '3 hours ago', ''),
+
+ ('-0000-00-01 00:00:00.00', '1 day ago', ''),
+ ('-0000-00-02 00:00:00.00', '2 days ago', ''),
+ ('-0000-00-03 00:00:00.00', '3 days ago', ''),
+
+ ('-0000-00-07 00:00:00.00', '1 week ago', ''),
+ ('-0000-00-14 00:00:00.00', '2 weeks ago', ''),
+ ('-0000-00-21 00:00:00.00', '3 weeks ago', ''),
+
+ ('-0000-01-00 00:00:00.00', '1 month ago', ''),
+ ('-0000-02-00 00:00:00.00', '2 months ago', ''),
+ ('-0000-03-00 00:00:00.00', '3 months ago', ''),
+ ('-0000-04-00 00:00:00.00', '4 months ago', ''),
+ ('-0000-05-00 00:00:00.00', '5 months ago', ''),
+ ('-0000-06-00 00:00:00.00', 'Half a year ago', ''),
+
+ ('-0001-00-00 00:00:00.00', '1 year ago', ''),
+ ]
+ fSelected = False;
+ for sTimestamp, sWayBackPointCaption, sExtraAttrs in aoWayBackPoints:
+ if sTimestamp == tsEffective:
+ fSelected = True;
+ sNavigation += ' <option value="%s"%s%s>%s</option>\n' \
+ % (webutils.quoteUrl(sTimestamp),
+ ' selected="selected"' if sTimestamp == tsEffective else '',
+ sExtraAttrs, sWayBackPointCaption, );
+ if not fSelected and tsEffective != '':
+ sNavigation += ' <option value="%s" selected>%s</option>\n' \
+ % (webutils.quoteUrl(tsEffective), WuiContentBase.formatTsShort(tsEffective))
+
+ sNavigation += ' </select>\n' \
+ '</form>\n';
+ return sNavigation;
+
+ @staticmethod
+ def generateTimeNavigationDateTime(sWhere, dParams, sNow):
+ """
+ Generates HTML for a form with date + time input fields.
+
+ Note! Modifies dParams!
+ """
+
+ #
+ # Date + time input fields. We use a java script helper to combine the two
+ # into a hidden field as there is no portable datetime input field type.
+ #
+ sNavigation = '<form method="get" action="?" onchange="timeNavigationUpdateHiddenEffDate(this,\'%s\')">' % (sWhere,);
+ if sNow is None:
+ sNow = utils.getIsoTimestamp();
+ else:
+ sNow = utils.normalizeIsoTimestampToZulu(sNow);
+ asSplit = sNow.split('T');
+ sNavigation += ' <input type="date" value="%s" id="EffDate%s"/> ' % (asSplit[0], sWhere, );
+ sNavigation += ' <input type="time" value="%s" id="EffTime%s"/> ' % (asSplit[1][:8], sWhere,);
+ sNavigation += ' <input type="hidden" name="%s" value="%s" id="EffDateTime%s"/>' \
+ % (WuiDispatcherBase.ksParamEffectiveDate, webutils.escapeAttr(sNow), sWhere);
+ for sKey in dParams:
+ sNavigation += ' <input type="hidden" name="%s" value="%s"/>' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(dParams[sKey]));
+ sNavigation += ' <input type="submit" value="Set"/>\n' \
+ '</form>\n';
+ return sNavigation;
+
+ ## @todo move to better place! WuiMain uses it.
+ @staticmethod
+ def generateTimeNavigation(sWhere, dParams, tsEffectiveAbs, sPreamble = '', sPostamble = '', fKeepPageNo = False):
+ """
+ Returns HTML for time navigation.
+
+ Note! Modifies dParams!
+ Note! Views without a need for a timescale just stubs this method.
+ """
+ sNavigation = '<div class="tmtimenav-%s tmtimenav">%s' % (sWhere, sPreamble,);
+
+ #
+ # Prepare the URL parameters.
+ #
+ if WuiDispatcherBase.ksParamPageNo in dParams: # Forget about page No when changing a period
+ del dParams[WuiDispatcherBase.ksParamPageNo]
+ if not fKeepPageNo and WuiDispatcherBase.ksParamEffectiveDate in dParams:
+ tsEffectiveParam = dParams[WuiDispatcherBase.ksParamEffectiveDate];
+ del dParams[WuiDispatcherBase.ksParamEffectiveDate];
+ else:
+ tsEffectiveParam = ''
+
+ #
+ # Generate the individual parts.
+ #
+ sNavigation += WuiListContentBase.generateTimeNavigationDateTime(sWhere, dParams, tsEffectiveAbs);
+ sNavigation += WuiListContentBase.generateTimeNavigationComboBox(sWhere, dParams, tsEffectiveParam);
+
+ sNavigation += '%s</div>' % (sPostamble,);
+ return sNavigation;
+
+ def _generateTimeNavigation(self, sWhere, sPreamble = '', sPostamble = ''):
+ """
+ Returns HTML for time navigation.
+
+ Note! Views without a need for a timescale just stubs this method.
+ """
+ return self.generateTimeNavigation(sWhere, self._oDisp.getParameters(), self._oDisp.getEffectiveDateParam(),
+ sPreamble, sPostamble)
+
+ @staticmethod
+ def generateItemPerPageSelector(sWhere, dParams, cCurItemsPerPage):
+ """
+ Generate HTML code for items per page selector.
+ Note! Modifies dParams!
+ """
+
+ # Drop the current page count parameter.
+ if WuiDispatcherBase.ksParamItemsPerPage in dParams:
+ del dParams[WuiDispatcherBase.ksParamItemsPerPage];
+
+ # Remove the current page number.
+ if WuiDispatcherBase.ksParamPageNo in dParams:
+ del dParams[WuiDispatcherBase.ksParamPageNo];
+
+ sHtmlItemsPerPageSelector = '<form name="TmItemsPerPageForm-%s" method="GET" class="tmitemsperpage-%s tmitemsperpage">\n'\
+ ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value;" title="Max items per page">\n' \
+ % (sWhere, WuiDispatcherBase.ksParamItemsPerPage, sWhere,
+ webutils.encodeUrlParams(dParams),
+ WuiDispatcherBase.ksParamItemsPerPage)
+
+ acItemsPerPage = [16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096];
+ for cItemsPerPage in acItemsPerPage:
+ sHtmlItemsPerPageSelector += ' <option value="%d" %s>%d per page</option>\n' \
+ % (cItemsPerPage,
+ 'selected="selected"' if cItemsPerPage == cCurItemsPerPage else '',
+ cItemsPerPage)
+ sHtmlItemsPerPageSelector += ' </select>\n' \
+ '</form>\n';
+
+ return sHtmlItemsPerPageSelector
+
+
+ def _generateNavigation(self, sWhere):
+ """
+ Return HTML for navigation.
+ """
+
+ #
+ # ASSUMES the dispatcher/controller code fetches one entry more than
+ # needed to fill the page to indicate further records.
+ #
+ sNavigation = '<div class="tmlistnav-%s">\n' % sWhere;
+ sNavigation += ' <table class="tmlistnavtab">\n' \
+ ' <tr>\n';
+ dParams = self._oDisp.getParameters();
+ dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage;
+ dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage;
+ if self._tsEffectiveDate is not None:
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._tsEffectiveDate;
+
+ # Prev
+ if self._iPage > 0:
+ dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage - 1;
+ sNavigation += ' <td align="left"><a href="?%s">Previous</a></td>\n' % (webutils.encodeUrlParams(dParams),);
+ else:
+ sNavigation += ' <td></td>\n';
+
+ # Time scale.
+ if self._fTimeNavigation:
+ sNavigation += '<td align="center" class="tmtimenav">';
+ sNavigation += self._generateTimeNavigation(sWhere);
+ sNavigation += '</td>';
+
+ # page count and next.
+ sNavigation += '<td align="right" class="tmnextanditemsperpage">\n';
+
+ if len(self._aoEntries) > self._cItemsPerPage:
+ dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1;
+ sNavigation += ' <a href="?%s">Next</a>\n' % (webutils.encodeUrlParams(dParams),);
+ sNavigation += self.generateItemPerPageSelector(sWhere, dParams, self._cItemsPerPage);
+ sNavigation += '</td>\n';
+ sNavigation += ' </tr>\n' \
+ ' </table>\n' \
+ '</div>\n';
+ return sNavigation;
+
+ def _checkSortingByColumnAscending(self, aiColumns):
+ """
+ Checks if we're sorting by this column.
+
+ Returns 0 if not sorting by this, negative if descending, positive if ascending. The
+ value indicates the priority (nearer to 0 is higher).
+ """
+ if len(aiColumns) <= len(self._aiSelectedSortColumns):
+ aiColumns = list(aiColumns);
+ aiNegColumns = list([-i for i in aiColumns]); # pylint: disable=consider-using-generator
+ i = 0;
+ while i + len(aiColumns) <= len(self._aiSelectedSortColumns):
+ aiSub = list(self._aiSelectedSortColumns[i : i + len(aiColumns)]);
+ if aiSub == aiColumns:
+ return 1 + i;
+ if aiSub == aiNegColumns:
+ return -1 - i;
+ i += 1;
+ return 0;
+
+ def _generateTableHeaders(self):
+ """
+ Generate table headers.
+ Returns raw html string.
+ Overridable.
+ """
+
+ sHtml = ' <thead class="tmheader"><tr>';
+ for iHeader, oHeader in enumerate(self._asColumnHeaders):
+ if isinstance(oHeader, WuiHtmlBase):
+ sHtml += '<th>' + oHeader.toHtml() + '</th>';
+ elif iHeader < len(self._aaiColumnSorting) and self._aaiColumnSorting[iHeader] is not None:
+ sHtml += '<th>'
+ iSorting = self._checkSortingByColumnAscending(self._aaiColumnSorting[iHeader]);
+ if iSorting > 0:
+ sDirection = '&nbsp;&#x25b4;' if iSorting == 1 else '<small>&nbsp;&#x25b5;</small>';
+ sSortParams = ','.join([str(-i) for i in self._aaiColumnSorting[iHeader]]);
+ else:
+ sDirection = '';
+ if iSorting < 0:
+ sDirection = '&nbsp;&#x25be;' if iSorting == -1 else '<small>&nbsp;&#x25bf;</small>'
+ sSortParams = ','.join([str(i) for i in self._aaiColumnSorting[iHeader]]);
+ sHtml += '<a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">' \
+ % (WuiDispatcherBase.ksParamSortColumns, sSortParams);
+ sHtml += webutils.escapeElem(oHeader) + '</a>' + sDirection + '</th>';
+ else:
+ sHtml += '<th>' + webutils.escapeElem(oHeader) + '</th>';
+ sHtml += '</tr><thead>\n';
+ return sHtml
+
+ def _generateTable(self):
+ """
+ show worker that just generates the table.
+ """
+
+ #
+ # Create a table.
+ # If no colum headers are provided, fall back on database field
+ # names, ASSUMING that the entries are ModelDataBase children.
+ # Note! the cellspacing is for IE8.
+ #
+ sPageBody = '<table class="tmtable" id="' + self._sId + '" cellspacing="0">\n';
+
+ if not self._asColumnHeaders:
+ self._asColumnHeaders = self._aoEntries[0].getDataAttributes();
+
+ sPageBody += self._generateTableHeaders();
+
+ #
+ # Format the body and close the table.
+ #
+ sPageBody += ' <tbody>\n';
+ for iEntry in range(min(len(self._aoEntries), self._cItemsPerPage)):
+ sPageBody += self._formatListEntryHtml(iEntry);
+ sPageBody += ' </tbody>\n' \
+ '</table>\n';
+ return sPageBody;
+
+ def _composeTitle(self):
+ """Composes the title string (return value)."""
+ sTitle = self._sTitle;
+ if self._iPage != 0:
+ sTitle += ' (page ' + unicode(self._iPage + 1) + ')'
+ if self._tsEffectiveDate is not None:
+ sTitle += ' as per ' + unicode(self.formatTsShort(self._tsEffectiveDate));
+ return sTitle;
+
+
+ def show(self, fShowNavigation = True):
+ """
+ Displays the list.
+ Returns (Title, HTML) on success, raises exception on error.
+ """
+
+ sPageBody = ''
+ if fShowNavigation:
+ sPageBody += self._generateNavigation('top');
+
+ if self._aoEntries:
+ sPageBody += self._generateTable();
+ if fShowNavigation:
+ sPageBody += self._generateNavigation('bottom');
+ else:
+ sPageBody += '<p>No entries.</p>'
+
+ return (self._composeTitle(), sPageBody);
+
+
+class WuiListContentWithActionBase(WuiListContentBase):
+ """
+ Base for the list content with action classes.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments
+ sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = sId,
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._aoActions = None; # List of [ oValue, sText, sHover ] provided by the child class.
+ self._sAction = None; # Set by the child class.
+ self._sCheckboxName = None; # Set by the child class.
+ self._asColumnHeaders = [ WuiRawHtml('<input type="checkbox" onClick="toggle%s(this)">'
+ % ('' if sId is None else sId)), ];
+ self._asColumnAttribs = [ 'align="center"', ];
+ self._aaiColumnSorting = [ None, ];
+
+ def _getCheckBoxColumn(self, iEntry, sValue):
+ """
+ Used by _formatListEntry implementations, returns a WuiRawHtmlBase object.
+ """
+ _ = iEntry;
+ return WuiRawHtml('<input type="checkbox" name="%s" value="%s">'
+ % (webutils.escapeAttr(self._sCheckboxName), webutils.escapeAttr(unicode(sValue))));
+
+ def show(self, fShowNavigation=True):
+ """
+ Displays the list.
+ Returns (Title, HTML) on success, raises exception on error.
+ """
+ assert self._aoActions is not None;
+ assert self._sAction is not None;
+
+ sPageBody = '<script language="JavaScript">\n' \
+ 'function toggle%s(oSource) {\n' \
+ ' aoCheckboxes = document.getElementsByName(\'%s\');\n' \
+ ' for(var i in aoCheckboxes)\n' \
+ ' aoCheckboxes[i].checked = oSource.checked;\n' \
+ '}\n' \
+ '</script>\n' \
+ % ('' if self._sId is None else self._sId, self._sCheckboxName,);
+ if fShowNavigation:
+ sPageBody += self._generateNavigation('top');
+ if self._aoEntries:
+
+ sPageBody += '<form action="?%s" method="post" class="tmlistactionform">\n' \
+ % (webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sAction,}),);
+ sPageBody += self._generateTable();
+
+ sPageBody += ' <label>Actions</label>\n' \
+ ' <select name="%s" id="%s-action-combo" class="tmlistactionform-combo">\n' \
+ % (webutils.escapeAttr(WuiDispatcherBase.ksParamListAction), webutils.escapeAttr(self._sId),);
+ for oValue, sText, _ in self._aoActions:
+ sPageBody += ' <option value="%s">%s</option>\n' \
+ % (webutils.escapeAttr(unicode(oValue)), webutils.escapeElem(sText), );
+ sPageBody += ' </select>\n';
+ sPageBody += ' <input type="submit"></input>\n';
+ sPageBody += '</form>\n';
+ if fShowNavigation:
+ sPageBody += self._generateNavigation('bottom');
+ else:
+ sPageBody += '<p>No entries.</p>'
+
+ return (self._composeTitle(), sPageBody);
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py
new file mode 100755
index 00000000..040b3217
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py
@@ -0,0 +1,660 @@
+# -*- coding: utf-8 -*-
+# $Id: wuigraphwiz.py $
+
+"""
+Test Manager WUI - Graph Wizard
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Python imports.
+import functools;
+
+# Validation Kit imports.
+from testmanager.webui.wuimain import WuiMain;
+from testmanager.webui.wuihlpgraph import WuiHlpLineGraphErrorbarY, WuiHlpGraphDataTableEx;
+from testmanager.webui.wuireport import WuiReportBase;
+
+from common import utils, webutils;
+from common import constants;
+
+
+class WuiGraphWiz(WuiReportBase):
+ """Construct a graph for analyzing test results (values) across builds and testboxes."""
+
+ ## @name Series name parts.
+ ## @{
+ kfSeriesName_TestBox = 1;
+ kfSeriesName_Product = 2;
+ kfSeriesName_Branch = 4;
+ kfSeriesName_BuildType = 8;
+ kfSeriesName_OsArchs = 16;
+ kfSeriesName_TestCase = 32;
+ kfSeriesName_TestCaseArgs = 64;
+ kfSeriesName_All = 127;
+ ## @}
+
+
+ def __init__(self, oModel, dParams, fSubReport = False, fnDPrint = None, oDisp = None):
+ WuiReportBase.__init__(self, oModel, dParams, fSubReport = fSubReport, fnDPrint = fnDPrint, oDisp = oDisp);
+
+ # Select graph implementation.
+ if dParams[WuiMain.ksParamGraphWizImpl] == 'charts':
+ from testmanager.webui.wuihlpgraphgooglechart import WuiHlpLineGraphErrorbarY as MyGraph;
+ self.oGraphClass = MyGraph;
+ elif dParams[WuiMain.ksParamGraphWizImpl] == 'matplotlib':
+ from testmanager.webui.wuihlpgraphmatplotlib import WuiHlpLineGraphErrorbarY as MyGraph;
+ self.oGraphClass = MyGraph;
+ else:
+ self.oGraphClass = WuiHlpLineGraphErrorbarY;
+
+
+ #
+ def _figureSeriesNameBits(self, aoSeries):
+ """ Figures out the method (bitmask) to use when naming series. """
+ if len(aoSeries) <= 1:
+ return WuiGraphWiz.kfSeriesName_TestBox;
+
+ # Start with all and drop unnecessary specs one-by-one.
+ fRet = WuiGraphWiz.kfSeriesName_All;
+
+ if [oSrs.idTestBox for oSrs in aoSeries].count(aoSeries[0].idTestBox) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestBox;
+
+ if [oSrs.idBuildCategory for oSrs in aoSeries].count(aoSeries[0].idBuildCategory) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_Product;
+ fRet &= ~WuiGraphWiz.kfSeriesName_Branch;
+ fRet &= ~WuiGraphWiz.kfSeriesName_BuildType;
+ fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs;
+ else:
+ if [oSrs.oBuildCategory.sProduct for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sProduct) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_Product;
+ if [oSrs.oBuildCategory.sBranch for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sBranch) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_Branch;
+ if [oSrs.oBuildCategory.sType for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sType) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_BuildType;
+
+ # Complicated.
+ fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs;
+ daTestBoxes = {};
+ for oSeries in aoSeries:
+ if oSeries.idTestBox in daTestBoxes:
+ daTestBoxes[oSeries.idTestBox].append(oSeries);
+ else:
+ daTestBoxes[oSeries.idTestBox] = [oSeries,];
+ for aoSeriesPerTestBox in daTestBoxes.values():
+ if len(aoSeriesPerTestBox) >= 0:
+ asOsArches = aoSeriesPerTestBox[0].oBuildCategory.asOsArches;
+ for i in range(1, len(aoSeriesPerTestBox)):
+ if aoSeriesPerTestBox[i].oBuildCategory.asOsArches != asOsArches:
+ fRet |= WuiGraphWiz.kfSeriesName_OsArchs;
+ break;
+
+ if aoSeries[0].oTestCaseArgs is None:
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs;
+ if [oSrs.idTestCase for oSrs in aoSeries].count(aoSeries[0].idTestCase) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestCase;
+ else:
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestCase;
+ if [oSrs.idTestCaseArgs for oSrs in aoSeries].count(aoSeries[0].idTestCaseArgs) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs;
+
+ return fRet;
+
+ def _getSeriesNameFromBits(self, oSeries, fBits):
+ """ Creates a series name from bits (kfSeriesName_xxx). """
+ assert fBits != 0;
+ sName = '';
+
+ if fBits & WuiGraphWiz.kfSeriesName_Product:
+ if sName: sName += ' / ';
+ sName += oSeries.oBuildCategory.sProduct;
+
+ if fBits & WuiGraphWiz.kfSeriesName_Branch:
+ if sName: sName += ' / ';
+ sName += oSeries.oBuildCategory.sBranch;
+
+ if fBits & WuiGraphWiz.kfSeriesName_BuildType:
+ if sName: sName += ' / ';
+ sName += oSeries.oBuildCategory.sType;
+
+ if fBits & WuiGraphWiz.kfSeriesName_OsArchs:
+ if sName: sName += ' / ';
+ sName += ' & '.join(oSeries.oBuildCategory.asOsArches);
+
+ if fBits & WuiGraphWiz.kfSeriesName_TestCaseArgs:
+ if sName: sName += ' / ';
+ if oSeries.idTestCaseArgs is not None:
+ sName += oSeries.oTestCase.sName + ':#' + str(oSeries.idTestCaseArgs);
+ else:
+ sName += oSeries.oTestCase.sName;
+ elif fBits & WuiGraphWiz.kfSeriesName_TestCase:
+ if sName: sName += ' / ';
+ sName += oSeries.oTestCase.sName;
+
+ if fBits & WuiGraphWiz.kfSeriesName_TestBox:
+ if sName: sName += ' / ';
+ sName += oSeries.oTestBox.sName;
+
+ return sName;
+
+ def _calcGraphName(self, oSeries, fSeriesName, sSampleName):
+ """ Constructs a name for the graph. """
+ fGraphName = ~fSeriesName & ( WuiGraphWiz.kfSeriesName_TestBox
+ | WuiGraphWiz.kfSeriesName_Product
+ | WuiGraphWiz.kfSeriesName_Branch
+ | WuiGraphWiz.kfSeriesName_BuildType
+ );
+ sName = self._getSeriesNameFromBits(oSeries, fGraphName);
+ if sName: sName += ' - ';
+ sName += sSampleName;
+ return sName;
+
+ def _calcSampleName(self, oCollection):
+ """ Constructs a name for a sample source (collection). """
+ if oCollection.sValue is not None:
+ asSampleName = [oCollection.sValue, 'in',];
+ elif oCollection.sType == self._oModel.ksTypeElapsed:
+ asSampleName = ['Elapsed time', 'for', ];
+ elif oCollection.sType == self._oModel.ksTypeResult:
+ asSampleName = ['Error count', 'for',];
+ else:
+ return 'Invalid collection type: "%s"' % (oCollection.sType,);
+
+ sTestName = ', '.join(oCollection.asTests if oCollection.asTests[0] else oCollection.asTests[1:]);
+ if sTestName == '':
+ # Use the testcase name if there is only one for all series.
+ if not oCollection.aoSeries:
+ return asSampleName[0];
+ if len(oCollection.aoSeries) > 1:
+ idTestCase = oCollection.aoSeries[0].idTestCase;
+ for oSeries in oCollection.aoSeries:
+ if oSeries.idTestCase != idTestCase:
+ return asSampleName[0];
+ sTestName = oCollection.aoSeries[0].oTestCase.sName;
+ return ' '.join(asSampleName) + ' ' + sTestName;
+
+
+ def _splitSeries(self, aoSeries):
+ """
+ Splits the data series (ReportGraphModel.DataSeries) into one or more graphs.
+
+ Returns an array of data series arrays.
+ """
+ # Must be at least two series for something to be splittable.
+ if len(aoSeries) <= 1:
+ if not aoSeries:
+ return [];
+ return [aoSeries,];
+
+ # Split on unit.
+ dUnitSeries = {};
+ for oSeries in aoSeries:
+ if oSeries.iUnit not in dUnitSeries:
+ dUnitSeries[oSeries.iUnit] = [];
+ dUnitSeries[oSeries.iUnit].append(oSeries);
+
+ # Sort the per-unit series since the build category was only sorted by ID.
+ for iUnit in dUnitSeries:
+ def mycmp(oSelf, oOther):
+ """ __cmp__ like function. """
+ iCmp = utils.stricmp(oSelf.oBuildCategory.sProduct, oOther.oBuildCategory.sProduct);
+ if iCmp != 0:
+ return iCmp;
+ iCmp = utils.stricmp(oSelf.oBuildCategory.sBranch, oOther.oBuildCategory.sBranch);
+ if iCmp != 0:
+ return iCmp;
+ iCmp = utils.stricmp(oSelf.oBuildCategory.sType, oOther.oBuildCategory.sType);
+ if iCmp != 0:
+ return iCmp;
+ iCmp = utils.stricmp(oSelf.oTestBox.sName, oOther.oTestBox.sName);
+ if iCmp != 0:
+ return iCmp;
+ return 0;
+ dUnitSeries[iUnit] = sorted(dUnitSeries[iUnit], key = functools.cmp_to_key(mycmp));
+
+ # Split the per-unit series up if necessary.
+ cMaxPerGraph = self._dParams[WuiMain.ksParamGraphWizMaxPerGraph];
+ aaoRet = [];
+ for aoUnitSeries in dUnitSeries.values():
+ while len(aoUnitSeries) > cMaxPerGraph:
+ aaoRet.append(aoUnitSeries[:cMaxPerGraph]);
+ aoUnitSeries = aoUnitSeries[cMaxPerGraph:];
+ if aoUnitSeries:
+ aaoRet.append(aoUnitSeries);
+
+ return aaoRet;
+
+ def _configureGraph(self, oGraph):
+ """
+ Configures oGraph according to user parameters and other config settings.
+
+ Returns oGraph.
+ """
+ oGraph.setWidth(self._dParams[WuiMain.ksParamGraphWizWidth])
+ oGraph.setHeight(self._dParams[WuiMain.ksParamGraphWizHeight])
+ oGraph.setDpi(self._dParams[WuiMain.ksParamGraphWizDpi])
+ oGraph.setErrorBarY(self._dParams[WuiMain.ksParamGraphWizErrorBarY]);
+ oGraph.setFontSize(self._dParams[WuiMain.ksParamGraphWizFontSize]);
+ if hasattr(oGraph, 'setXkcdStyle'):
+ oGraph.setXkcdStyle(self._dParams[WuiMain.ksParamGraphWizXkcdStyle]);
+
+ return oGraph;
+
+ def _generateInteractiveForm(self):
+ """
+ Generates the HTML for the interactive form.
+ Returns (sTopOfForm, sEndOfForm)
+ """
+
+ #
+ # The top of the form.
+ #
+ sTop = '<form action="#" method="get" id="graphwiz-form">\n' \
+ ' <input type="hidden" name="%s" value="%s"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ % ( WuiMain.ksParamAction, WuiMain.ksActionGraphWiz,
+ WuiMain.ksParamGraphWizSrcTestSetId, self._dParams[WuiMain.ksParamGraphWizSrcTestSetId],
+ );
+
+ sTop += ' <div id="graphwiz-nav">\n';
+ sTop += ' <script type="text/javascript">\n' \
+ ' window.onresize = function(){ return graphwizOnResizeRecalcWidth("graphwiz-nav", "%s"); }\n' \
+ ' window.onload = function(){ return graphwizOnLoadRememberWidth("graphwiz-nav"); }\n' \
+ ' </script>\n' \
+ % ( WuiMain.ksParamGraphWizWidth, );
+
+ #
+ # Top: First row.
+ #
+ sTop += ' <div id="graphwiz-top-1">\n';
+
+ # time.
+ sNow = self._dParams[WuiMain.ksParamEffectiveDate];
+ if sNow is None: sNow = '';
+ sTop += ' <div id="graphwiz-time">\n';
+ sTop += ' <label for="%s">Starting:</label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-time-input"/>\n' \
+ % ( WuiMain.ksParamEffectiveDate,
+ WuiMain.ksParamEffectiveDate, WuiMain.ksParamEffectiveDate, sNow, );
+
+ sTop += ' <input type="hidden" name="%s" value="%u"/>\n' % ( WuiMain.ksParamReportPeriods, 1, );
+ sTop += ' <label for="%s"> Going back:\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-period-input"/>\n' \
+ % ( WuiMain.ksParamReportPeriodInHours,
+ WuiMain.ksParamReportPeriodInHours, WuiMain.ksParamReportPeriodInHours,
+ utils.formatIntervalHours(self._dParams[WuiMain.ksParamReportPeriodInHours]) );
+ sTop += ' </div>\n';
+
+ # Graph options top row.
+ sTop += ' <div id="graphwiz-top-options-1">\n';
+
+ # graph type.
+ sTop += ' <label for="%s">Graph:</label>\n' \
+ ' <select name="%s" id="%s">\n' \
+ % ( WuiMain.ksParamGraphWizImpl, WuiMain.ksParamGraphWizImpl, WuiMain.ksParamGraphWizImpl, );
+ for (sImpl, sDesc) in WuiMain.kaasGraphWizImplCombo:
+ sTop += ' <option value="%s"%s>%s</option>\n' \
+ % (sImpl, ' selected' if sImpl == self._dParams[WuiMain.ksParamGraphWizImpl] else '', sDesc);
+ sTop += ' </select>\n';
+
+ # graph size.
+ sTop += ' <label for="%s">Graph size:</label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-pixel-input"> x\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-pixel-input">\n' \
+ ' <label for="%s">Dpi:</label>'\
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-dpi-input">\n' \
+ ' <button type="button" onclick="%s">Defaults</button>\n' \
+ % ( WuiMain.ksParamGraphWizWidth,
+ WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizWidth, self._dParams[WuiMain.ksParamGraphWizWidth],
+ WuiMain.ksParamGraphWizHeight, WuiMain.ksParamGraphWizHeight, self._dParams[WuiMain.ksParamGraphWizHeight],
+ WuiMain.ksParamGraphWizDpi,
+ WuiMain.ksParamGraphWizDpi, WuiMain.ksParamGraphWizDpi, self._dParams[WuiMain.ksParamGraphWizDpi],
+ webutils.escapeAttr('return graphwizSetDefaultSizeValues("graphwiz-nav", "%s", "%s", "%s");'
+ % ( WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizHeight,
+ WuiMain.ksParamGraphWizDpi )),
+ );
+
+ sTop += ' </div>\n'; # (options row 1)
+
+ sTop += ' </div>\n'; # (end of row 1)
+
+ #
+ # Top: Second row.
+ #
+ sTop += ' <div id="graphwiz-top-2">\n';
+
+ # Submit
+ sFormButton = '<button type="submit">Refresh</button>\n';
+ sTop += ' <div id="graphwiz-top-submit">' + sFormButton + '</div>\n';
+
+
+ # Options.
+ sTop += ' <div id="graphwiz-top-options-2">\n';
+
+ sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s/>\n' \
+ ' <label for="%s">Tabular data</label>\n' \
+ % ( WuiMain.ksParamGraphWizTabular, WuiMain.ksParamGraphWizTabular,
+ ' checked' if self._dParams[WuiMain.ksParamGraphWizTabular] else '',
+ WuiMain.ksParamGraphWizTabular);
+
+ if hasattr(self.oGraphClass, 'setXkcdStyle'):
+ sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s/>\n' \
+ ' <label for="%s">xkcd-style</label>\n' \
+ % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle,
+ ' checked' if self._dParams[WuiMain.ksParamGraphWizXkcdStyle] else '',
+ WuiMain.ksParamGraphWizXkcdStyle);
+ elif self._dParams[WuiMain.ksParamGraphWizXkcdStyle]:
+ sTop += ' <input type="hidden" name="%s" id="%s" value="1"/>\n' \
+ % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle, );
+
+ if not hasattr(self.oGraphClass, 'kfNoErrorBarsSupport'):
+ sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s title="%s"/>\n' \
+ ' <label for="%s">Error bars,</label>\n' \
+ ' <label for="%s">max: </label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-maxerrorbar-input" title="%s"/>\n' \
+ % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY,
+ ' checked' if self._dParams[WuiMain.ksParamGraphWizErrorBarY] else '',
+ 'Error bars shows some of the max and min results on the Y-axis.',
+ WuiMain.ksParamGraphWizErrorBarY,
+ WuiMain.ksParamGraphWizMaxErrorBarY,
+ WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY,
+ self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY],
+ 'Maximum number of Y-axis error bar per graph. (Too many makes it unreadable.)'
+ );
+ else:
+ if self._dParams[WuiMain.ksParamGraphWizErrorBarY]:
+ sTop += '<input type="hidden" name="%s" id="%s" value="1">\n' \
+ % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY, );
+ sTop += '<input type="hidden" name="%s" id="%s" value="%u">\n' \
+ % ( WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY,
+ self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY], );
+
+ sTop += ' <label for="%s">Font size: </label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-fontsize-input"/>\n' \
+ % ( WuiMain.ksParamGraphWizFontSize,
+ WuiMain.ksParamGraphWizFontSize, WuiMain.ksParamGraphWizFontSize,
+ self._dParams[WuiMain.ksParamGraphWizFontSize], );
+
+ sTop += ' <label for="%s">Data series: </label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-maxpergraph-input" title="%s"/>\n' \
+ % ( WuiMain.ksParamGraphWizMaxPerGraph,
+ WuiMain.ksParamGraphWizMaxPerGraph, WuiMain.ksParamGraphWizMaxPerGraph,
+ self._dParams[WuiMain.ksParamGraphWizMaxPerGraph],
+ 'Max data series per graph.' );
+
+ sTop += ' </div>\n'; # (options row 2)
+
+ sTop += ' </div>\n'; # (end of row 2)
+
+ sTop += ' </div>\n'; # end of top.
+
+ #
+ # The end of the page selection.
+ #
+ sEnd = ' <div id="graphwiz-end-selection">\n';
+
+ #
+ # Testbox selection
+ #
+ aidTestBoxes = list(self._dParams[WuiMain.ksParamGraphWizTestBoxIds]);
+ sEnd += ' <div id="graphwiz-testboxes" class="graphwiz-end-selection-group">\n' \
+ ' <h3>TestBox Selection:</h3>\n' \
+ ' <ol class="tmgraph-testboxes">\n';
+
+ # Get a list of eligible testboxes from the DB.
+ for oTestBox in self._oModel.getEligibleTestBoxes():
+ try: aidTestBoxes.remove(oTestBox.idTestBox);
+ except: sChecked = '';
+ else: sChecked = ' checked';
+ sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-tb-%u"%s/>' \
+ '<label for="gw-tb-%u">%s</label></li>\n' \
+ % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox, sChecked,
+ oTestBox.idTestBox, oTestBox.sName);
+
+ # List testboxes that have been checked in a different period or something.
+ for idTestBox in aidTestBoxes:
+ oTestBox = self._oModel.oCache.getTestBox(idTestBox);
+ sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-tb-%u" checked/>' \
+ '<label for="gw-tb-%u">%s</label></li>\n' \
+ % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox,
+ oTestBox.idTestBox, oTestBox.sName);
+
+ sEnd += ' </ol>\n' \
+ ' </div>\n';
+
+ #
+ # Build category selection.
+ #
+ aidBuildCategories = list(self._dParams[WuiMain.ksParamGraphWizBuildCatIds]);
+ sEnd += ' <div id="graphwiz-buildcategories" class="graphwiz-end-selection-group">\n' \
+ ' <h3>Build Category Selection:</h3>\n' \
+ ' <ol class="tmgraph-buildcategories">\n';
+ for oBuildCat in self._oModel.getEligibleBuildCategories():
+ try: aidBuildCategories.remove(oBuildCat.idBuildCategory);
+ except: sChecked = '';
+ else: sChecked = ' checked';
+ sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-bc-%u" %s/>' \
+ '<label for="gw-bc-%u">%s / %s / %s / %s</label></li>\n' \
+ % ( WuiMain.ksParamGraphWizBuildCatIds, oBuildCat.idBuildCategory, oBuildCat.idBuildCategory, sChecked,
+ oBuildCat.idBuildCategory,
+ oBuildCat.sProduct, oBuildCat.sBranch, oBuildCat.sType, ' & '.join(oBuildCat.asOsArches) );
+ assert not aidBuildCategories; # SQL should return all currently selected.
+
+ sEnd += ' </ol>\n' \
+ ' </div>\n';
+
+ #
+ # Testcase variations.
+ #
+ sEnd += ' <div id="graphwiz-testcase-variations" class="graphwiz-end-selection-group">\n' \
+ ' <h3>Miscellaneous:</h3>\n' \
+ ' <ol>';
+
+ sEnd += ' <li>\n' \
+ ' <input type="checkbox" id="%s" name="%s" value="1"%s/>\n' \
+ ' <label for="%s">Separate by testcase variation.</label>\n' \
+ ' </li>\n' \
+ % ( WuiMain.ksParamGraphWizSepTestVars, WuiMain.ksParamGraphWizSepTestVars,
+ ' checked' if self._dParams[WuiMain.ksParamGraphWizSepTestVars] else '',
+ WuiMain.ksParamGraphWizSepTestVars );
+
+
+ sEnd += ' <li>\n' \
+ ' <lable for="%s">Test case ID:</label>\n' \
+ ' <input type="text" id="%s" name="%s" value="%s" readonly/>\n' \
+ ' </li>\n' \
+ % ( WuiMain.ksParamGraphWizTestCaseIds,
+ WuiMain.ksParamGraphWizTestCaseIds, WuiMain.ksParamGraphWizTestCaseIds,
+ ','.join([str(i) for i in self._dParams[WuiMain.ksParamGraphWizTestCaseIds]]), );
+
+ sEnd += ' </ol>\n' \
+ ' </div>\n';
+
+ #sEnd += ' <h3>&nbsp;</h3>\n';
+
+ #
+ # Finish up the form.
+ #
+ sEnd += ' <div id="graphwiz-end-submit"><p>' + sFormButton + '</p></div>\n';
+ sEnd += ' </div>\n' \
+ '</form>\n';
+
+ return (sTop, sEnd);
+
+ def generateReportBody(self):
+ fInteractive = not self._fSubReport;
+
+ # Quick mockup.
+ self._sTitle = 'Graph Wizzard';
+
+ sHtml = '';
+ sHtml += '<h2>Incomplete code - no complaints yet, thank you!!</h2>\n';
+
+ #
+ # Create a form for altering the data we're working with.
+ #
+ if fInteractive:
+ (sTopOfForm, sEndOfForm) = self._generateInteractiveForm();
+ sHtml += sTopOfForm;
+ del sTopOfForm;
+
+ #
+ # Emit the graphs. At least one per sample source.
+ #
+ sHtml += ' <div id="graphwiz-graphs">\n';
+ iGraph = 0;
+ aoCollections = self._oModel.fetchGraphData();
+ for iCollection, oCollection in enumerate(aoCollections):
+ # Name the graph and add a checkbox for removing it.
+ sSampleName = self._calcSampleName(oCollection);
+ sHtml += ' <div class="graphwiz-collection" id="graphwiz-source-%u">\n' % (iCollection,);
+ if fInteractive:
+ sHtml += ' <div class="graphwiz-src-select">\n' \
+ ' <input type="checkbox" name="%s" id="%s" value="%s:%s%s" checked class="graphwiz-src-input">\n' \
+ ' <label for="%s">%s</label>\n' \
+ ' </div>\n' \
+ % ( WuiMain.ksParamReportSubjectIds, WuiMain.ksParamReportSubjectIds, oCollection.sType,
+ ':'.join([str(idStr) for idStr in oCollection.aidStrTests]),
+ ':%u' % oCollection.idStrValue if oCollection.idStrValue else '',
+ WuiMain.ksParamReportSubjectIds, sSampleName );
+
+ if oCollection.aoSeries:
+ #
+ # Split the series into sub-graphs as needed and produce SVGs.
+ #
+ aaoSeries = self._splitSeries(oCollection.aoSeries);
+ for aoSeries in aaoSeries:
+ # Gather the data for this graph. (Most big stuff is passed by
+ # reference, so there shouldn't be any large memory penalty for
+ # repacking the data here.)
+ sYUnit = None;
+ if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0:
+ sYUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit];
+ oData = WuiHlpGraphDataTableEx(sXUnit = 'Build revision', sYUnit = sYUnit);
+
+ fSeriesName = self._figureSeriesNameBits(aoSeries);
+ for oSeries in aoSeries:
+ sSeriesName = self._getSeriesNameFromBits(oSeries, fSeriesName);
+ asHtmlTooltips = None;
+ if len(oSeries.aoRevInfo) == len(oSeries.aiRevisions):
+ asHtmlTooltips = [];
+ for i, oRevInfo in enumerate(oSeries.aoRevInfo):
+ sPlusMinus = '';
+ if oSeries.acSamples[i] > 1:
+ sPlusMinus = ' (+%s/-%s; %u samples)' \
+ % ( utils.formatNumber(oSeries.aiErrorBarAbove[i]),
+ utils.formatNumber(oSeries.aiErrorBarBelow[i]),
+ oSeries.acSamples[i])
+ sTooltip = '<table class=\'graphwiz-tt\'><tr><td>%s:</td><td>%s %s %s</td></tr>'\
+ '<tr><td>Rev:</td><td>r%s</td></tr>' \
+ % ( sSeriesName,
+ utils.formatNumber(oSeries.aiValues[i]),
+ sYUnit, sPlusMinus,
+ oSeries.aiRevisions[i],
+ );
+ if oRevInfo.sAuthor is not None:
+ sMsg = oRevInfo.sMessage[:80].strip();
+ #if sMsg.find('\n') >= 0:
+ # sMsg = sMsg[:sMsg.find('\n')].strip();
+ sTooltip += '<tr><td>Author:</td><td>%s</td></tr>' \
+ '<tr><td>Date:</td><td>%s</td><tr>' \
+ '<tr><td>Message:</td><td>%s%s</td></tr>' \
+ % ( oRevInfo.sAuthor,
+ self.formatTsShort(oRevInfo.tsCreated),
+ sMsg, '...' if len(oRevInfo.sMessage) > len(sMsg) else '');
+ sTooltip += '</table>';
+ asHtmlTooltips.append(sTooltip);
+ oData.addDataSeries(sSeriesName, oSeries.aiRevisions, oSeries.aiValues, asHtmlTooltips,
+ oSeries.aiErrorBarBelow, oSeries.aiErrorBarAbove);
+ # Render the data into a graph.
+ oGraph = self.oGraphClass('tmgraph-%u' % (iGraph,), oData, self._oDisp);
+ self._configureGraph(oGraph);
+
+ oGraph.setTitle(self._calcGraphName(aoSeries[0], fSeriesName, sSampleName));
+ sHtml += ' <div class="graphwiz-graph" id="graphwiz-graph-%u">\n' % (iGraph,);
+ sHtml += oGraph.renderGraph();
+ sHtml += '\n </div>\n';
+ iGraph += 1;
+
+ #
+ # Emit raw tabular data if requested.
+ #
+ if self._dParams[WuiMain.ksParamGraphWizTabular]:
+ sHtml += ' <div class="graphwiz-tab-div" id="graphwiz-tab-%u">\n' \
+ ' <table class="tmtable graphwiz-tab">\n' \
+ % (iCollection, );
+ for aoSeries in aaoSeries:
+ if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0:
+ sUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit];
+ else:
+ sUnit = str(aoSeries[0].iUnit);
+
+ for iSeries, oSeries in enumerate(aoSeries):
+ sColor = self.oGraphClass.calcSeriesColor(iSeries);
+
+ sHtml += '<thead class="tmheader">\n' \
+ ' <tr class="graphwiz-tab graphwiz-tab-new-series-row">\n' \
+ ' <th colspan="5"><span style="background-color:%s;">&nbsp;&nbsp;</span> %s</th>\n' \
+ ' </tr>\n' \
+ ' <tr class="graphwiz-tab graphwiz-tab-col-hdr-row">\n' \
+ ' <th>Revision</th><th>Value (%s)</th><th>&Delta;max</th><th>&Delta;min</th>' \
+ '<th>Samples</th>\n' \
+ ' </tr>\n' \
+ '</thead>\n' \
+ % ( sColor,
+ self._getSeriesNameFromBits(oSeries, self.kfSeriesName_All & ~self.kfSeriesName_OsArchs),
+ sUnit );
+
+ for i, iRevision in enumerate(oSeries.aiRevisions):
+ sHtml += ' <tr class="%s"><td>r%s</td><td>%s</td><td>+%s</td><td>-%s</td><td>%s</td></tr>\n' \
+ % ( 'tmodd' if i & 1 else 'tmeven',
+ iRevision, oSeries.aiValues[i],
+ oSeries.aiErrorBarAbove[i], oSeries.aiErrorBarBelow[i],
+ oSeries.acSamples[i]);
+ sHtml += ' </table>\n' \
+ ' </div>\n';
+ else:
+ sHtml += '<i>No results.</i>\n';
+ sHtml += ' </div>\n'
+ sHtml += ' </div>\n';
+
+ #
+ # Finish the form.
+ #
+ if fInteractive:
+ sHtml += sEndOfForm;
+
+ return sHtml;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py
new file mode 100755
index 00000000..38015e83
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py
@@ -0,0 +1,1111 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpform.py $
+
+"""
+Test Manager Web-UI - Form Helpers.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports.
+import copy;
+import sys;
+
+# Validation Kit imports.
+from common import utils;
+from common.webutils import escapeAttr, escapeElem;
+from testmanager import config;
+from testmanager.core.schedgroup import SchedGroupMemberData, SchedGroupDataEx;
+from testmanager.core.testcaseargs import TestCaseArgsData;
+from testmanager.core.testgroup import TestGroupMemberData, TestGroupDataEx;
+from testmanager.core.testbox import TestBoxDataForSchedGroup;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ unicode = str; # pylint: disable=redefined-builtin,invalid-name
+
+
+class WuiHlpForm(object):
+ """
+ Helper for constructing a form.
+ """
+
+ ksItemsList = 'ksItemsList'
+
+ ksOnSubmit_AddReturnToFieldWithCurrentUrl = '+AddReturnToFieldWithCurrentUrl+';
+
+ def __init__(self, sId, sAction, dErrors = None, fReadOnly = False, sOnSubmit = None):
+ self._fFinalized = False;
+ self._fReadOnly = fReadOnly;
+ self._dErrors = dErrors if dErrors is not None else {};
+
+ if sOnSubmit == self.ksOnSubmit_AddReturnToFieldWithCurrentUrl:
+ sOnSubmit = u'return addRedirectToInputFieldWithCurrentUrl(this)';
+ if sOnSubmit is None: sOnSubmit = u'';
+ else: sOnSubmit = u' onsubmit=\"%s\"' % (escapeAttr(sOnSubmit),);
+
+ self._sBody = u'\n' \
+ u'<div id="%s" class="tmform">\n' \
+ u' <form action="%s" method="post"%s>\n' \
+ u' <ul>\n' \
+ % (sId, sAction, sOnSubmit);
+
+ def _add(self, sText):
+ """Internal worker for appending text to the body."""
+ assert not self._fFinalized;
+ if not self._fFinalized:
+ self._sBody += utils.toUnicode(sText, errors='ignore');
+ return True;
+ return False;
+
+ def _escapeErrorText(self, sText):
+ """Escapes error text, preserving some predefined HTML tags."""
+ if sText.find('<br>') >= 0:
+ asParts = sText.split('<br>');
+ for i, _ in enumerate(asParts):
+ asParts[i] = escapeElem(asParts[i].strip());
+ sText = '<br>\n'.join(asParts);
+ else:
+ sText = escapeElem(sText);
+ return sText;
+
+ def _addLabel(self, sName, sLabel, sDivSubClass = 'normal'):
+ """Internal worker for adding a label."""
+ if sName in self._dErrors:
+ sError = self._dErrors[sName];
+ if utils.isString(sError): # List error trick (it's an associative array).
+ return self._add(u' <li>\n'
+ u' <div class="tmform-field"><div class="tmform-field-%s">\n'
+ u' <label for="%s" class="tmform-error-label">%s\n'
+ u' <span class="tmform-error-desc">%s</span>\n'
+ u' </label>\n'
+ % (escapeAttr(sDivSubClass), escapeAttr(sName), escapeElem(sLabel),
+ self._escapeErrorText(sError), ) );
+ return self._add(u' <li>\n'
+ u' <div class="tmform-field"><div class="tmform-field-%s">\n'
+ u' <label for="%s">%s</label>\n'
+ % (escapeAttr(sDivSubClass), escapeAttr(sName), escapeElem(sLabel)) );
+
+
+ def finalize(self):
+ """
+ Finalizes the form and returns the body.
+ """
+ if not self._fFinalized:
+ self._add(u' </ul>\n'
+ u' </form>\n'
+ u'</div>\n'
+ u'<div class="clear"></div>\n' );
+ return self._sBody;
+
+ def addTextHidden(self, sName, sValue, sExtraAttribs = ''):
+ """Adds a hidden text input."""
+ return self._add(u' <div class="tmform-field-hidden">\n'
+ u' <input name="%s" id="%s" type="text" hidden%s value="%s" class="tmform-hidden">\n'
+ u' </div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeElem(str(sValue)) ));
+ #
+ # Non-input stuff.
+ #
+ def addNonText(self, sValue, sLabel, sName = 'non-text', sPostHtml = ''):
+ """Adds a read-only text input."""
+ self._addLabel(sName, sLabel, 'string');
+ if sValue is None: sValue = '';
+ return self._add(u' <p>%s%s</p>\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % (escapeElem(unicode(sValue)), sPostHtml ));
+
+ def addRawHtml(self, sRawHtml, sLabel, sName = 'raw-html'):
+ """Adds a read-only text input."""
+ self._addLabel(sName, sLabel, 'string');
+ self._add(sRawHtml);
+ return self._add(u' </div></div>\n'
+ u' </li>\n');
+
+
+ #
+ # Text input fields.
+ #
+ def addText(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = '', sPostHtml = ''):
+ """Adds a text input."""
+ if self._fReadOnly:
+ return self.addTextRO(sName, sValue, sLabel, sSubClass, sExtraAttribs);
+ if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass);
+ self._addLabel(sName, sLabel, sSubClass);
+ if sValue is None: sValue = '';
+ return self._add(u' <input name="%s" id="%s" type="text"%s value="%s">%s\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeAttr(unicode(sValue)), sPostHtml ));
+
+ def addTextRO(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = '', sPostHtml = ''):
+ """Adds a read-only text input."""
+ if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass);
+ self._addLabel(sName, sLabel, sSubClass);
+ if sValue is None: sValue = '';
+ return self._add(u' <input name="%s" id="%s" type="text" readonly%s value="%s" class="tmform-input-readonly">'
+ u'%s\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeAttr(unicode(sValue)), sPostHtml ));
+
+ def addWideText(self, sName, sValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a wide text input."""
+ return self.addText(sName, sValue, sLabel, 'wide', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addWideTextRO(self, sName, sValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a wide read-only text input."""
+ return self.addTextRO(sName, sValue, sLabel, 'wide', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def _adjustMultilineTextAttribs(self, sExtraAttribs, sValue):
+ """ Internal helper for setting good default sizes for textarea based on content."""
+ if sExtraAttribs.find('cols') < 0 and sExtraAttribs.find('width') < 0:
+ sExtraAttribs = 'cols="96%" ' + sExtraAttribs;
+
+ if sExtraAttribs.find('rows') < 0 and sExtraAttribs.find('width') < 0:
+ if sValue is None: sValue = '';
+ else: sValue = sValue.strip();
+
+ cRows = sValue.count('\n') + (not sValue.endswith('\n'));
+ if cRows * 80 < len(sValue):
+ cRows += 2;
+ cRows = max(min(cRows, 16), 2);
+ sExtraAttribs = ('rows="%s" ' % (cRows,)) + sExtraAttribs;
+
+ return sExtraAttribs;
+
+ def addMultilineText(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = ''):
+ """Adds a multiline text input."""
+ if self._fReadOnly:
+ return self.addMultilineTextRO(sName, sValue, sLabel, sSubClass, sExtraAttribs);
+ if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass)
+ self._addLabel(sName, sLabel, sSubClass)
+ if sValue is None: sValue = '';
+ sNewValue = unicode(sValue) if not isinstance(sValue, list) else '\n'.join(sValue)
+ return self._add(u' <textarea name="%s" id="%s" %s>%s</textarea>\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), self._adjustMultilineTextAttribs(sExtraAttribs, sNewValue),
+ escapeElem(sNewValue)))
+
+ def addMultilineTextRO(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = ''):
+ """Adds a multiline read-only text input."""
+ if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass)
+ self._addLabel(sName, sLabel, sSubClass)
+ if sValue is None: sValue = '';
+ sNewValue = unicode(sValue) if not isinstance(sValue, list) else '\n'.join(sValue)
+ return self._add(u' <textarea name="%s" id="%s" readonly %s>%s</textarea>\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), self._adjustMultilineTextAttribs(sExtraAttribs, sNewValue),
+ escapeElem(sNewValue)))
+
+ def addInt(self, sName, iValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds an integer input."""
+ return self.addText(sName, unicode(iValue), sLabel, 'int', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addIntRO(self, sName, iValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds an integer input."""
+ return self.addTextRO(sName, unicode(iValue), sLabel, 'int', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addLong(self, sName, lValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a long input."""
+ return self.addText(sName, unicode(lValue), sLabel, 'long', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addLongRO(self, sName, lValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a long input."""
+ return self.addTextRO(sName, unicode(lValue), sLabel, 'long', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addUuid(self, sName, uuidValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds an UUID input."""
+ return self.addText(sName, unicode(uuidValue), sLabel, 'uuid', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addUuidRO(self, sName, uuidValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a read-only UUID input."""
+ return self.addTextRO(sName, unicode(uuidValue), sLabel, 'uuid', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addTimestampRO(self, sName, sTimestamp, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a read-only database string timstamp input."""
+ return self.addTextRO(sName, sTimestamp, sLabel, 'timestamp', sExtraAttribs, sPostHtml = sPostHtml);
+
+
+ #
+ # Text areas.
+ #
+
+
+ #
+ # Combo boxes.
+ #
+ def addComboBox(self, sName, sSelected, sLabel, aoOptions, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a combo box."""
+ if self._fReadOnly:
+ return self.addComboBoxRO(sName, sSelected, sLabel, aoOptions, sExtraAttribs, sPostHtml);
+ self._addLabel(sName, sLabel, 'combobox');
+ self._add(' <select name="%s" id="%s" class="tmform-combobox"%s>\n'
+ % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs));
+ sSelected = unicode(sSelected);
+ for iValue, sText, _ in aoOptions:
+ sValue = unicode(iValue);
+ self._add(' <option value="%s"%s>%s</option>\n'
+ % (escapeAttr(sValue), ' selected' if sValue == sSelected else '',
+ escapeElem(sText)));
+ return self._add(u' </select>' + sPostHtml + '\n'
+ u' </div></div>\n'
+ u' </li>\n');
+
+ def addComboBoxRO(self, sName, sSelected, sLabel, aoOptions, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a read-only combo box."""
+ self.addTextHidden(sName, sSelected);
+ self._addLabel(sName, sLabel, 'combobox-readonly');
+ self._add(u' <select name="%s" id="%s" disabled class="tmform-combobox"%s>\n'
+ % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs));
+ sSelected = unicode(sSelected);
+ for iValue, sText, _ in aoOptions:
+ sValue = unicode(iValue);
+ self._add(' <option value="%s"%s>%s</option>\n'
+ % (escapeAttr(sValue), ' selected' if sValue == sSelected else '',
+ escapeElem(sText)));
+ return self._add(u' </select>' + sPostHtml + '\n'
+ u' </div></div>\n'
+ u' </li>\n');
+
+ #
+ # Check boxes.
+ #
+ @staticmethod
+ def _reinterpretBool(fValue):
+ """Reinterprets a value as a boolean type."""
+ if fValue is not type(True):
+ if fValue is None:
+ fValue = False;
+ elif str(fValue) in ('True', 'true', '1'):
+ fValue = True;
+ else:
+ fValue = False;
+ return fValue;
+
+ def addCheckBox(self, sName, fChecked, sLabel, sExtraAttribs = ''):
+ """Adds an check box."""
+ if self._fReadOnly:
+ return self.addCheckBoxRO(sName, fChecked, sLabel, sExtraAttribs);
+ self._addLabel(sName, sLabel, 'checkbox');
+ fChecked = self._reinterpretBool(fChecked);
+ return self._add(u' <input name="%s" id="%s" type="checkbox"%s%s value="1" class="tmform-checkbox">\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % (escapeAttr(sName), escapeAttr(sName), ' checked' if fChecked else '', sExtraAttribs));
+
+ def addCheckBoxRO(self, sName, fChecked, sLabel, sExtraAttribs = ''):
+ """Adds an readonly check box."""
+ self._addLabel(sName, sLabel, 'checkbox');
+ fChecked = self._reinterpretBool(fChecked);
+ # Hack Alert! The onclick and onkeydown are for preventing editing and fake readonly/disabled.
+ return self._add(u' <input name="%s" id="%s" type="checkbox"%s readonly%s value="1" class="readonly"\n'
+ u' onclick="return false" onkeydown="return false">\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % (escapeAttr(sName), escapeAttr(sName), ' checked' if fChecked else '', sExtraAttribs));
+
+ #
+ # List of items to check
+ #
+ def _addList(self, sName, aoRows, sLabel, fUseTable = False, sId = 'dummy', sExtraAttribs = ''):
+ """
+ Adds a list of items to check.
+
+ @param sName Name of HTML form element
+ @param aoRows List of [sValue, fChecked, sName] sub-arrays.
+ @param sLabel Label of HTML form element
+ """
+ fReadOnly = self._fReadOnly; ## @todo add this as a parameter.
+ if fReadOnly:
+ sExtraAttribs += ' readonly onclick="return false" onkeydown="return false"';
+
+ self._addLabel(sName, sLabel, 'list');
+ if not aoRows:
+ return self._add('No items</div></div></li>')
+ sNameEscaped = escapeAttr(sName);
+
+ self._add(' <div class="tmform-checkboxes-container" id="%s">\n' % (escapeAttr(sId),));
+ if fUseTable:
+ self._add(' <table>\n');
+ for asRow in aoRows:
+ assert len(asRow) == 3; # Don't allow sloppy input data!
+ fChecked = self._reinterpretBool(asRow[1])
+ self._add(u' <tr>\n'
+ u' <td><input type="checkbox" name="%s" value="%s"%s%s></td>\n'
+ u' <td>%s</td>\n'
+ u' </tr>\n'
+ % ( sNameEscaped, escapeAttr(unicode(asRow[0])), ' checked' if fChecked else '', sExtraAttribs,
+ escapeElem(unicode(asRow[2])), ));
+ self._add(u' </table>\n');
+ else:
+ for asRow in aoRows:
+ assert len(asRow) == 3; # Don't allow sloppy input data!
+ fChecked = self._reinterpretBool(asRow[1])
+ self._add(u' <div class="tmform-checkbox-holder">'
+ u'<input type="checkbox" name="%s" value="%s"%s%s> %s</input></div>\n'
+ % ( sNameEscaped, escapeAttr(unicode(asRow[0])), ' checked' if fChecked else '', sExtraAttribs,
+ escapeElem(unicode(asRow[2])),));
+ return self._add(u' </div></div></div>\n'
+ u' </li>\n');
+
+
+ def addListOfOsArches(self, sName, aoOsArches, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for OS/ARCH selection.
+ asOsArches is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoOsArches, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-os-arches',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfTypes(self, sName, aoTypes, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for build type selection.
+ aoTypes is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoTypes, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-build-types',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfTestCases(self, sName, aoTestCases, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for test box (dependency) selection.
+ aoTestCases is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoTestCases, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-testcases',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfResources(self, sName, aoTestCases, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for resource selection.
+ aoTestCases is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoTestCases, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-resources',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfTestGroups(self, sName, aoTestGroups, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for test group selection.
+ aoTestGroups is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoTestGroups, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-testgroups',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfTestCaseArgs(self, sName, aoVariations, sLabel): # pylint: disable=too-many-statements
+ """
+ Adds a list of test case argument variations to the form.
+
+ @param sName Name of HTML form element
+ @param aoVariations List of TestCaseArgsData instances.
+ @param sLabel Label of HTML form element
+ """
+ self._addLabel(sName, sLabel);
+
+ sTableId = u'TestArgsExtendingListRoot';
+ fReadOnly = self._fReadOnly; ## @todo argument?
+ sReadOnlyAttr = u' readonly class="tmform-input-readonly"' if fReadOnly else '';
+
+ sHtml = u'<li>\n'
+
+ #
+ # Define javascript function for extending the list of test case
+ # variations. Doing it here so we can use the python constants. This
+ # also permits multiple argument lists on one page should that ever be
+ # required...
+ #
+ if not fReadOnly:
+ sHtml += u'<script type="text/javascript">\n'
+ sHtml += u'\n';
+ sHtml += u'g_%s_aItems = { %s };\n' % (sName, ', '.join(('%s: 1' % (i,)) for i in range(len(aoVariations))),);
+ sHtml += u'g_%s_cItems = %s;\n' % (sName, len(aoVariations),);
+ sHtml += u'g_%s_iIdMod = %s;\n' % (sName, len(aoVariations) + 32);
+ sHtml += u'\n';
+ sHtml += u'function %s_removeEntry(sId)\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' if (g_%s_cItems > 1)\n' % (sName,);
+ sHtml += u' {\n';
+ sHtml += u' g_%s_cItems--;\n' % (sName,);
+ sHtml += u' delete g_%s_aItems[sId];\n' % (sName,);
+ sHtml += u' setElementValueToKeyList(\'%s\', g_%s_aItems);\n' % (sName, sName);
+ sHtml += u'\n';
+ for iInput in range(8):
+ sHtml += u' removeHtmlNode(\'%s[\' + sId + \'][%s]\');\n' % (sName, iInput,);
+ sHtml += u' }\n';
+ sHtml += u'}\n';
+ sHtml += u'\n';
+ sHtml += u'function %s_extendListEx(sSubName, cGangMembers, cSecTimeout, sArgs, sTestBoxReqExpr, sBuildReqExpr)\n' \
+ % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' var oElement = document.getElementById(\'%s\');\n' % (sTableId,);
+ sHtml += u' var oTBody = document.createElement(\'tbody\');\n';
+ sHtml += u' var sHtml = \'\';\n';
+ sHtml += u' var sId;\n';
+ sHtml += u'\n';
+ sHtml += u' g_%s_iIdMod += 1;\n' % (sName,);
+ sHtml += u' sId = g_%s_iIdMod.toString();\n' % (sName,);
+
+ oVarDefaults = TestCaseArgsData();
+ oVarDefaults.convertToParamNull();
+ sHtml += u'\n';
+ sHtml += u' sHtml += \'<tr class="tmform-testcasevars-first-row">\';\n';
+ sHtml += u' sHtml += \' <td>Sub-Name:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-subname">' \
+ '<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][0]" value="\' + sSubName + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_sSubName, sName,);
+ sHtml += u' sHtml += \' <td>Gang Members:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-tiny-int">' \
+ '<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][0]" value="\' + cGangMembers + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_cGangMembers, sName,);
+ sHtml += u' sHtml += \' <td>Timeout:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-int">' \
+ u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][1]" value="\'+ cSecTimeout + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_cSecTimeout, sName,);
+ sHtml += u' sHtml += \' <td><a href="#" onclick="%s_removeEntry(\\\'\' + sId + \'\\\');"> Remove</a></td>\';\n' \
+ % (sName, );
+ sHtml += u' sHtml += \' <td></td>\';\n';
+ sHtml += u' sHtml += \'</tr>\';\n'
+ sHtml += u'\n';
+ sHtml += u' sHtml += \'<tr class="tmform-testcasevars-inner-row">\';\n';
+ sHtml += u' sHtml += \' <td>Arguments:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sArgs + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_sArgs, sName,);
+ sHtml += u' sHtml += \' <td></td>\';\n';
+ sHtml += u' sHtml += \'</tr>\';\n'
+ sHtml += u'\n';
+ sHtml += u' sHtml += \'<tr class="tmform-testcasevars-inner-row">\';\n';
+ sHtml += u' sHtml += \' <td>TestBox Reqs:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sTestBoxReqExpr' \
+ u' + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_sTestBoxReqExpr, sName,);
+ sHtml += u' sHtml += \' <td></td>\';\n';
+ sHtml += u' sHtml += \'</tr>\';\n'
+ sHtml += u'\n';
+ sHtml += u' sHtml += \'<tr class="tmform-testcasevars-final-row">\';\n';
+ sHtml += u' sHtml += \' <td>Build Reqs:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sBuildReqExpr + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_sBuildReqExpr, sName,);
+ sHtml += u' sHtml += \' <td></td>\';\n';
+ sHtml += u' sHtml += \'</tr>\';\n'
+ sHtml += u'\n';
+ sHtml += u' oTBody.id = \'%s[\' + sId + \'][6]\';\n' % (sName,);
+ sHtml += u' oTBody.innerHTML = sHtml;\n';
+ sHtml += u'\n';
+ sHtml += u' oElement.appendChild(oTBody);\n';
+ sHtml += u'\n';
+ sHtml += u' g_%s_aItems[sId] = 1;\n' % (sName,);
+ sHtml += u' g_%s_cItems++;\n' % (sName,);
+ sHtml += u' setElementValueToKeyList(\'%s\', g_%s_aItems);\n' % (sName, sName);
+ sHtml += u'}\n';
+ sHtml += u'function %s_extendList()\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' %s_extendListEx("%s", "%s", "%s", "%s", "%s", "%s");\n' % (sName,
+ escapeAttr(unicode(oVarDefaults.sSubName)), escapeAttr(unicode(oVarDefaults.cGangMembers)),
+ escapeAttr(unicode(oVarDefaults.cSecTimeout)), escapeAttr(oVarDefaults.sArgs),
+ escapeAttr(oVarDefaults.sTestBoxReqExpr), escapeAttr(oVarDefaults.sBuildReqExpr), );
+ sHtml += u'}\n';
+ if config.g_kfVBoxSpecific:
+ sSecTimeoutDef = escapeAttr(unicode(oVarDefaults.cSecTimeout));
+ sHtml += u'function vbox_%s_add_uni()\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' %s_extendListEx("1-raw", "1", "%s", "--cpu-counts 1 --virt-modes raw", ' \
+ u' "", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("1-hw", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt", ' \
+ u' "fCpuHwVirt is True", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("1-np", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt-np", ' \
+ u' "fCpuNestedPaging is True", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u'}\n';
+ sHtml += u'function vbox_%s_add_uni_amd64()\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' %s_extendListEx("1-hw", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt", ' \
+ u' "fCpuHwVirt is True", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("1-np", "%s", "--cpu-counts 1 --virt-modes hwvirt-np", ' \
+ u' "fCpuNestedPaging is True", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u'}\n';
+ sHtml += u'function vbox_%s_add_smp()\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' %s_extendListEx("2-hw", "1", "%s", "--cpu-counts 2 --virt-modes hwvirt",' \
+ u' "fCpuHwVirt is True and cCpus >= 2", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("2-np", "1", "%s", "--cpu-counts 2 --virt-modes hwvirt-np",' \
+ u' "fCpuNestedPaging is True and cCpus >= 2", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("3-hw", "1", "%s", "--cpu-counts 3 --virt-modes hwvirt",' \
+ u' "fCpuHwVirt is True and cCpus >= 3", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("4-np", "1", "%s", "--cpu-counts 4 --virt-modes hwvirt-np ",' \
+ u' "fCpuNestedPaging is True and cCpus >= 4", "");\n' % (sName, sSecTimeoutDef);
+ #sHtml += u' %s_extendListEx("6-hw", "1", "%s", "--cpu-counts 6 --virt-modes hwvirt",' \
+ # u' "fCpuHwVirt is True and cCpus >= 6", "");\n' % (sName, sSecTimeoutDef);
+ #sHtml += u' %s_extendListEx("8-np", "1", "%s", "--cpu-counts 8 --virt-modes hwvirt-np",' \
+ # u' "fCpuNestedPaging is True and cCpus >= 8", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u'}\n';
+ sHtml += u'</script>\n';
+
+
+ #
+ # List current entries.
+ #
+ sHtml += u'<input type="hidden" name="%s" id="%s" value="%s">\n' \
+ % (sName, sName, ','.join(unicode(i) for i in range(len(aoVariations))), );
+ sHtml += u' <table id="%s" class="tmform-testcasevars">\n' % (sTableId,)
+ if not fReadOnly:
+ sHtml += u' <caption>\n' \
+ u' <a href="#" onClick="%s_extendList()">Add</a>\n' % (sName,);
+ if config.g_kfVBoxSpecific:
+ sHtml += u' [<a href="#" onClick="vbox_%s_add_uni()">Single CPU Variations</a>\n' % (sName,);
+ sHtml += u' <a href="#" onClick="vbox_%s_add_uni_amd64()">amd64</a>]\n' % (sName,);
+ sHtml += u' [<a href="#" onClick="vbox_%s_add_smp()">SMP Variations</a>]\n' % (sName,);
+ sHtml += u' </caption>\n';
+
+ dSubErrors = {};
+ if sName in self._dErrors and isinstance(self._dErrors[sName], dict):
+ dSubErrors = self._dErrors[sName];
+
+ for iVar, _ in enumerate(aoVariations):
+ oVar = copy.copy(aoVariations[iVar]);
+ oVar.convertToParamNull();
+
+ sHtml += u'<tbody id="%s[%s][6]">\n' % (sName, iVar,)
+ sHtml += u' <tr class="tmform-testcasevars-first-row">\n' \
+ u' <td>Sub-name:</td>' \
+ u' <td class="tmform-field-subname"><input name="%s[%s][%s]" id="%s[%s][1]" value="%s"%s></td>\n' \
+ u' <td>Gang Members:</td>' \
+ u' <td class="tmform-field-tiny-int"><input name="%s[%s][%s]" id="%s[%s][1]" value="%s"%s></td>\n' \
+ u' <td>Timeout:</td>' \
+ u' <td class="tmform-field-int"><input name="%s[%s][%s]" id="%s[%s][2]" value="%s"%s></td>\n' \
+ % ( sName, iVar, TestCaseArgsData.ksParam_sSubName, sName, iVar, oVar.sSubName, sReadOnlyAttr,
+ sName, iVar, TestCaseArgsData.ksParam_cGangMembers, sName, iVar, oVar.cGangMembers, sReadOnlyAttr,
+ sName, iVar, TestCaseArgsData.ksParam_cSecTimeout, sName, iVar,
+ utils.formatIntervalSeconds2(oVar.cSecTimeout), sReadOnlyAttr, );
+ if not fReadOnly:
+ sHtml += u' <td><a href="#" onclick="%s_removeEntry(\'%s\');">Remove</a></td>\n' \
+ % (sName, iVar);
+ else:
+ sHtml += u' <td></td>\n';
+ sHtml += u' <td class="tmform-testcasevars-stupid-border-column"></td>\n' \
+ u' </tr>\n';
+
+ sHtml += u' <tr class="tmform-testcasevars-inner-row">\n' \
+ u' <td>Arguments:</td>' \
+ u' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[%s][%s]" id="%s[%s][3]" value="%s"%s></td>\n' \
+ u' <td></td>\n' \
+ u' </tr>\n' \
+ % ( sName, iVar, TestCaseArgsData.ksParam_sArgs, sName, iVar, escapeAttr(oVar.sArgs), sReadOnlyAttr)
+
+ sHtml += u' <tr class="tmform-testcasevars-inner-row">\n' \
+ u' <td>TestBox Reqs:</td>' \
+ u' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[%s][%s]" id="%s[%s][4]" value="%s"%s></td>\n' \
+ u' <td></td>\n' \
+ u' </tr>\n' \
+ % ( sName, iVar, TestCaseArgsData.ksParam_sTestBoxReqExpr, sName, iVar,
+ escapeAttr(oVar.sTestBoxReqExpr), sReadOnlyAttr)
+
+ sHtml += u' <tr class="tmform-testcasevars-final-row">\n' \
+ u' <td>Build Reqs:</td>' \
+ u' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[%s][%s]" id="%s[%s][5]" value="%s"%s></td>\n' \
+ u' <td></td>\n' \
+ u' </tr>\n' \
+ % ( sName, iVar, TestCaseArgsData.ksParam_sBuildReqExpr, sName, iVar,
+ escapeAttr(oVar.sBuildReqExpr), sReadOnlyAttr)
+
+
+ if iVar in dSubErrors:
+ sHtml += u' <tr><td colspan="4"><p align="left" class="tmform-error-desc">%s</p></td></tr>\n' \
+ % (self._escapeErrorText(dSubErrors[iVar]),);
+
+ sHtml += u'</tbody>\n';
+ sHtml += u' </table>\n'
+ sHtml += u'</li>\n'
+
+ return self._add(sHtml)
+
+ def addListOfTestGroupMembers(self, sName, aoTestGroupMembers, aoAllTestCases, sLabel, # pylint: disable=too-many-locals
+ fReadOnly = True):
+ """
+ For WuiTestGroup.
+ """
+ assert len(aoTestGroupMembers) <= len(aoAllTestCases);
+ self._addLabel(sName, sLabel);
+ if not aoAllTestCases:
+ return self._add('<li>No testcases.</li>\n')
+
+ self._add(u'<input name="%s" type="hidden" value="%s">\n'
+ % ( TestGroupDataEx.ksParam_aidTestCases,
+ ','.join([unicode(oTestCase.idTestCase) for oTestCase in aoAllTestCases]), ));
+
+ self._add(u'<table class="tmformtbl">\n'
+ u' <thead>\n'
+ u' <tr>\n'
+ u' <th rowspan="2"></th>\n'
+ u' <th rowspan="2">Test Case</th>\n'
+ u' <th rowspan="2">All Vars</th>\n'
+ u' <th rowspan="2">Priority [0..31]</th>\n'
+ u' <th colspan="4" align="center">Variations</th>\n'
+ u' </tr>\n'
+ u' <tr>\n'
+ u' <th>Included</th>\n'
+ u' <th>Gang size</th>\n'
+ u' <th>Timeout</th>\n'
+ u' <th>Arguments</th>\n'
+ u' </tr>\n'
+ u' </thead>\n'
+ u' <tbody>\n'
+ );
+
+ if self._fReadOnly:
+ fReadOnly = True;
+ sCheckBoxAttr = ' readonly onclick="return false" onkeydown="return false"' if fReadOnly else '';
+
+ oDefMember = TestGroupMemberData();
+ aoTestGroupMembers = list(aoTestGroupMembers); # Copy it so we can pop.
+ for iTestCase, _ in enumerate(aoAllTestCases):
+ oTestCase = aoAllTestCases[iTestCase];
+
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoTestGroupMembers):
+ if aoTestGroupMembers[i].oTestCase.idTestCase == oTestCase.idTestCase:
+ oMember = aoTestGroupMembers.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrefix = u'%s[%d]' % (sName, oTestCase.idTestCase,);
+ self._add(u' <tr class="%s">\n'
+ u' <td rowspan="%d">\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestCase
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ u' </td>\n'
+ % ( 'tmodd' if iTestCase & 1 else 'tmeven',
+ len(oTestCase.aoTestCaseArgs),
+ sPrefix, TestGroupMemberData.ksParam_idTestCase, oTestCase.idTestCase,
+ sPrefix, TestGroupMemberData.ksParam_idTestGroup, -1 if oMember is None else oMember.idTestGroup,
+ sPrefix, TestGroupMemberData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrefix, TestGroupMemberData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrefix, TestGroupMemberData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ TestGroupDataEx.ksParam_aoMembers, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oTestCase.idTestCase, oTestCase.idTestCase, escapeElem(oTestCase.sName),
+ ));
+ self._add(u' <td rowspan="%d" align="left">%s</td>\n'
+ % ( len(oTestCase.aoTestCaseArgs), escapeElem(oTestCase.sName), ));
+
+ self._add(u' <td rowspan="%d" title="Include all variations (checked) or choose a set?">\n'
+ u' <input name="%s[%s]" type="checkbox"%s%s value="-1">\n'
+ u' </td>\n'
+ % ( len(oTestCase.aoTestCaseArgs),
+ sPrefix, TestGroupMemberData.ksParam_aidTestCaseArgs,
+ ' checked' if oMember is None or oMember.aidTestCaseArgs is None else '', sCheckBoxAttr, ));
+
+ self._add(u' <td rowspan="%d" align="center">\n'
+ u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n'
+ u' </td>\n'
+ % ( len(oTestCase.aoTestCaseArgs),
+ sPrefix, TestGroupMemberData.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '', ));
+
+ # Argument variations.
+ aidTestCaseArgs = [] if oMember is None or oMember.aidTestCaseArgs is None else oMember.aidTestCaseArgs;
+ for iVar, oVar in enumerate(oTestCase.aoTestCaseArgs):
+ if iVar > 0:
+ self._add(' <tr class="%s">\n' % ('tmodd' if iTestCase & 1 else 'tmeven',));
+ self._add(u' <td align="center">\n'
+ u' <input name="%s[%s]" type="checkbox"%s%s value="%d">'
+ u' </td>\n'
+ % ( sPrefix, TestGroupMemberData.ksParam_aidTestCaseArgs,
+ ' checked' if oVar.idTestCaseArgs in aidTestCaseArgs else '', sCheckBoxAttr, oVar.idTestCaseArgs,
+ ));
+ self._add(u' <td align="center">%s</td>\n'
+ u' <td align="center">%s</td>\n'
+ u' <td align="left">%s</td>\n'
+ % ( oVar.cGangMembers,
+ 'Default' if oVar.cSecTimeout is None else oVar.cSecTimeout,
+ escapeElem(oVar.sArgs) ));
+
+ self._add(u' </tr>\n');
+
+
+
+ if not oTestCase.aoTestCaseArgs:
+ self._add(u' <td></td> <td></td> <td></td> <td></td>\n'
+ u' </tr>\n');
+ return self._add(u' </tbody>\n'
+ u'</table>\n');
+
+ def addListOfSchedGroupMembers(self, sName, aoSchedGroupMembers, aoAllRelevantTestGroups, # pylint: disable=too-many-locals
+ sLabel, idSchedGroup, fReadOnly = True):
+ """
+ For WuiAdminSchedGroup.
+ """
+ if fReadOnly is None or self._fReadOnly:
+ fReadOnly = self._fReadOnly;
+ assert len(aoSchedGroupMembers) <= len(aoAllRelevantTestGroups);
+ self._addLabel(sName, sLabel);
+ if not aoAllRelevantTestGroups:
+ return self._add(u'<li>No test groups.</li>\n')
+
+ self._add(u'<input name="%s" type="hidden" value="%s">\n'
+ % ( SchedGroupDataEx.ksParam_aidTestGroups,
+ ','.join([unicode(oTestGroup.idTestGroup) for oTestGroup in aoAllRelevantTestGroups]), ));
+
+ self._add(u'<table class="tmformtbl tmformtblschedgroupmembers">\n'
+ u' <thead>\n'
+ u' <tr>\n'
+ u' <th></th>\n'
+ u' <th>Test Group</th>\n'
+ u' <th>Priority [0..31]</th>\n'
+ u' <th>Prerequisite Test Group</th>\n'
+ u' <th>Weekly schedule</th>\n'
+ u' </tr>\n'
+ u' </thead>\n'
+ u' <tbody>\n'
+ );
+
+ sCheckBoxAttr = u' readonly onclick="return false" onkeydown="return false"' if fReadOnly else '';
+ sComboBoxAttr = u' disabled' if fReadOnly else '';
+
+ oDefMember = SchedGroupMemberData();
+ aoSchedGroupMembers = list(aoSchedGroupMembers); # Copy it so we can pop.
+ for iTestGroup, _ in enumerate(aoAllRelevantTestGroups):
+ oTestGroup = aoAllRelevantTestGroups[iTestGroup];
+
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoSchedGroupMembers):
+ if aoSchedGroupMembers[i].oTestGroup.idTestGroup == oTestGroup.idTestGroup:
+ oMember = aoSchedGroupMembers.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrefix = u'%s[%d]' % (sName, oTestGroup.idTestGroup,);
+ self._add(u' <tr class="%s">\n'
+ u' <td>\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ u' </td>\n'
+ % ( 'tmodd' if iTestGroup & 1 else 'tmeven',
+ sPrefix, SchedGroupMemberData.ksParam_idTestGroup, oTestGroup.idTestGroup,
+ sPrefix, SchedGroupMemberData.ksParam_idSchedGroup, idSchedGroup,
+ sPrefix, SchedGroupMemberData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrefix, SchedGroupMemberData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrefix, SchedGroupMemberData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ SchedGroupDataEx.ksParam_aoMembers, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oTestGroup.idTestGroup, oTestGroup.idTestGroup, escapeElem(oTestGroup.sName),
+ ));
+ self._add(u' <td>%s</td>\n' % ( escapeElem(oTestGroup.sName), ));
+
+ self._add(u' <td>\n'
+ u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n'
+ u' </td>\n'
+ % ( sPrefix, SchedGroupMemberData.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '', ));
+
+ self._add(u' <td>\n'
+ u' <select name="%s[%s]" id="%s[%s]" class="tmform-combobox"%s>\n'
+ u' <option value="-1"%s>None</option>\n'
+ % ( sPrefix, SchedGroupMemberData.ksParam_idTestGroupPreReq,
+ sPrefix, SchedGroupMemberData.ksParam_idTestGroupPreReq,
+ sComboBoxAttr,
+ ' selected' if oMember is None or oMember.idTestGroupPreReq is None else '',
+ ));
+ for oTestGroup2 in aoAllRelevantTestGroups:
+ if oTestGroup2 != oTestGroup:
+ fSelected = oMember is not None and oTestGroup2.idTestGroup == oMember.idTestGroupPreReq;
+ self._add(' <option value="%s"%s>%s</option>\n'
+ % ( oTestGroup2.idTestGroup, ' selected' if fSelected else '', escapeElem(oTestGroup2.sName), ));
+ self._add(u' </select>\n'
+ u' </td>\n');
+
+ self._add(u' <td>\n'
+ u' Todo<input name="%s[%s]" type="hidden" value="%s">\n'
+ u' </td>\n'
+ % ( sPrefix, SchedGroupMemberData.ksParam_bmHourlySchedule,
+ '' if oMember is None else oMember.bmHourlySchedule, ));
+
+ self._add(u' </tr>\n');
+ return self._add(u' </tbody>\n'
+ u'</table>\n');
+
+ def addListOfSchedGroupBoxes(self, sName, aoSchedGroupBoxes, # pylint: disable=too-many-locals
+ aoAllRelevantTestBoxes, sLabel, idSchedGroup, fReadOnly = True,
+ fUseTable = False): # (str, list[TestBoxDataEx], list[TestBoxDataEx], str, bool, bool) -> str
+ """
+ For WuiAdminSchedGroup.
+ """
+ if fReadOnly is None or self._fReadOnly:
+ fReadOnly = self._fReadOnly;
+ assert len(aoSchedGroupBoxes) <= len(aoAllRelevantTestBoxes);
+ self._addLabel(sName, sLabel);
+ if not aoAllRelevantTestBoxes:
+ return self._add(u'<li>No test boxes.</li>\n')
+
+ self._add(u'<input name="%s" type="hidden" value="%s">\n'
+ % ( SchedGroupDataEx.ksParam_aidTestBoxes,
+ ','.join([unicode(oTestBox.idTestBox) for oTestBox in aoAllRelevantTestBoxes]), ));
+
+ sCheckBoxAttr = u' readonly onclick="return false" onkeydown="return false"' if fReadOnly else '';
+ oDefMember = TestBoxDataForSchedGroup();
+ aoSchedGroupBoxes = list(aoSchedGroupBoxes); # Copy it so we can pop.
+
+ from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLink;
+
+ if not fUseTable:
+ #
+ # Non-table version (see also addListOfOsArches).
+ #
+ self._add(' <div class="tmform-checkboxes-container">\n');
+
+ for iTestBox, oTestBox in enumerate(aoAllRelevantTestBoxes):
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoSchedGroupBoxes):
+ if aoSchedGroupBoxes[i].oTestBox and aoSchedGroupBoxes[i].oTestBox.idTestBox == oTestBox.idTestBox:
+ oMember = aoSchedGroupBoxes.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrf = u'%s[%d]' % (sName, oTestBox.idTestBox,);
+ self._add(u' <div class="tmform-checkbox-holder tmshade%u">\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ % ( iTestBox & 7,
+ sPrf, TestBoxDataForSchedGroup.ksParam_idTestBox, oTestBox.idTestBox,
+ sPrf, TestBoxDataForSchedGroup.ksParam_idSchedGroup, idSchedGroup,
+ sPrf, TestBoxDataForSchedGroup.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrf, TestBoxDataForSchedGroup.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrf, TestBoxDataForSchedGroup.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ SchedGroupDataEx.ksParam_aoTestBoxes, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oTestBox.idTestBox, oTestBox.idTestBox, escapeElem(oTestBox.sName),
+ ));
+
+ self._add(u' <span class="tmform-priority tmform-testbox-priority">'
+ u'<input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s title="%s"></span>\n'
+ % ( sPrf, TestBoxDataForSchedGroup.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '',
+ escapeAttr("Priority [0..31]. Higher value means run more often.") ));
+
+ self._add(u' <span class="tmform-testbox-name">%s</span>\n'
+ % ( WuiTestBoxDetailsLink(oTestBox, sName = '%s (%s)' % (oTestBox.sName, oTestBox.sOs,)),));
+ self._add(u' </div>\n');
+ return self._add(u' </div></div></div>\n'
+ u' </li>\n');
+
+ #
+ # Table version.
+ #
+ self._add(u'<table class="tmformtbl">\n'
+ u' <thead>\n'
+ u' <tr>\n'
+ u' <th></th>\n'
+ u' <th>Test Box</th>\n'
+ u' <th>Priority [0..31]</th>\n'
+ u' </tr>\n'
+ u' </thead>\n'
+ u' <tbody>\n'
+ );
+
+ for iTestBox, oTestBox in enumerate(aoAllRelevantTestBoxes):
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoSchedGroupBoxes):
+ if aoSchedGroupBoxes[i].oTestBox and aoSchedGroupBoxes[i].oTestBox.idTestBox == oTestBox.idTestBox:
+ oMember = aoSchedGroupBoxes.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrefix = u'%s[%d]' % (sName, oTestBox.idTestBox,);
+ self._add(u' <tr class="%s">\n'
+ u' <td>\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ u' </td>\n'
+ % ( 'tmodd' if iTestBox & 1 else 'tmeven',
+ sPrefix, TestBoxDataForSchedGroup.ksParam_idTestBox, oTestBox.idTestBox,
+ sPrefix, TestBoxDataForSchedGroup.ksParam_idSchedGroup, idSchedGroup,
+ sPrefix, TestBoxDataForSchedGroup.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrefix, TestBoxDataForSchedGroup.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrefix, TestBoxDataForSchedGroup.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ SchedGroupDataEx.ksParam_aoTestBoxes, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oTestBox.idTestBox, oTestBox.idTestBox, escapeElem(oTestBox.sName),
+ ));
+ self._add(u' <td align="left">%s</td>\n' % ( escapeElem(oTestBox.sName), ));
+
+ self._add(u' <td align="center">\n'
+ u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n'
+ u' </td>\n'
+ % ( sPrefix,
+ TestBoxDataForSchedGroup.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '', ));
+
+ self._add(u' </tr>\n');
+ return self._add(u' </tbody>\n'
+ u'</table>\n');
+
+ def addListOfSchedGroupsForTestBox(self, sName, aoInSchedGroups, aoAllSchedGroups, sLabel, # pylint: disable=too-many-locals
+ idTestBox, fReadOnly = None):
+ # type: (str, TestBoxInSchedGroupDataEx, SchedGroupData, str, bool) -> str
+ """
+ For WuiTestGroup.
+ """
+ from testmanager.core.testbox import TestBoxInSchedGroupData, TestBoxDataEx;
+
+ if fReadOnly is None or self._fReadOnly:
+ fReadOnly = self._fReadOnly;
+ assert len(aoInSchedGroups) <= len(aoAllSchedGroups);
+
+ # Only show selected groups in read-only mode.
+ if fReadOnly:
+ aoAllSchedGroups = [oCur.oSchedGroup for oCur in aoInSchedGroups]
+
+ self._addLabel(sName, sLabel);
+ if not aoAllSchedGroups:
+ return self._add('<li>No scheduling groups.</li>\n')
+
+ # Add special parameter with all the scheduling group IDs in the form.
+ self._add(u'<input name="%s" type="hidden" value="%s">\n'
+ % ( TestBoxDataEx.ksParam_aidSchedGroups,
+ ','.join([unicode(oSchedGroup.idSchedGroup) for oSchedGroup in aoAllSchedGroups]), ));
+
+ # Table header.
+ self._add(u'<table class="tmformtbl">\n'
+ u' <thead>\n'
+ u' <tr>\n'
+ u' <th rowspan="2"></th>\n'
+ u' <th rowspan="2">Schedulding Group</th>\n'
+ u' <th rowspan="2">Priority [0..31]</th>\n'
+ u' </tr>\n'
+ u' </thead>\n'
+ u' <tbody>\n'
+ );
+
+ # Table body.
+ if self._fReadOnly:
+ fReadOnly = True;
+ sCheckBoxAttr = ' readonly onclick="return false" onkeydown="return false"' if fReadOnly else '';
+
+ oDefMember = TestBoxInSchedGroupData();
+ aoInSchedGroups = list(aoInSchedGroups); # Copy it so we can pop.
+ for iSchedGroup, oSchedGroup in enumerate(aoAllSchedGroups):
+
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoInSchedGroups):
+ if aoInSchedGroups[i].idSchedGroup == oSchedGroup.idSchedGroup:
+ oMember = aoInSchedGroups.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrefix = u'%s[%d]' % (sName, oSchedGroup.idSchedGroup,);
+ self._add(u' <tr class="%s">\n'
+ u' <td>\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ u' </td>\n'
+ % ( 'tmodd' if iSchedGroup & 1 else 'tmeven',
+ sPrefix, TestBoxInSchedGroupData.ksParam_idSchedGroup, oSchedGroup.idSchedGroup,
+ sPrefix, TestBoxInSchedGroupData.ksParam_idTestBox, idTestBox,
+ sPrefix, TestBoxInSchedGroupData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrefix, TestBoxInSchedGroupData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrefix, TestBoxInSchedGroupData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ TestBoxDataEx.ksParam_aoInSchedGroups, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oSchedGroup.idSchedGroup, oSchedGroup.idSchedGroup, escapeElem(oSchedGroup.sName),
+ ));
+ self._add(u' <td align="left">%s</td>\n' % ( escapeElem(oSchedGroup.sName), ));
+
+ self._add(u' <td align="center">\n'
+ u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n'
+ u' </td>\n'
+ % ( sPrefix, TestBoxInSchedGroupData.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '', ));
+ self._add(u' </tr>\n');
+
+ return self._add(u' </tbody>\n'
+ u'</table>\n');
+
+
+ #
+ # Buttons.
+ #
+ def addSubmit(self, sLabel = 'Submit'):
+ """Adds the submit button to the form."""
+ if self._fReadOnly:
+ return True;
+ return self._add(u' <li>\n'
+ u' <br>\n'
+ u' <div class="tmform-field"><div class="tmform-field-submit">\n'
+ u' <label>&nbsp;</label>\n'
+ u' <input type="submit" value="%s">\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % (escapeElem(sLabel),));
+
+ def addReset(self):
+ """Adds a reset button to the form."""
+ if self._fReadOnly:
+ return True;
+ return self._add(u' <li>\n'
+ u' <div class="tmform-button"><div class="tmform-button-reset">\n'
+ u' <input type="reset" value="%s">\n'
+ u' </div></div>\n'
+ u' </li>\n');
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py
new file mode 100755
index 00000000..ed08bb0f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraph.py $
+
+"""
+Test Manager Web-UI - Graph Helpers.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+class WuiHlpGraphDataTable(object): # pylint: disable=too-few-public-methods
+ """
+ Data table container.
+ """
+
+ class Row(object): # pylint: disable=too-few-public-methods
+ """A row."""
+ def __init__(self, sGroup, aoValues, asValues = None):
+ self.sName = sGroup;
+ self.aoValues = aoValues;
+ if asValues is None:
+ self.asValues = [str(oVal) for oVal in aoValues];
+ else:
+ assert len(asValues) == len(aoValues);
+ self.asValues = asValues;
+
+ def __init__(self, sGroupLable, asMemberLabels):
+ self.aoTable = [ WuiHlpGraphDataTable.Row(sGroupLable, asMemberLabels), ];
+ self.fHasStringValues = False;
+
+ def addRow(self, sGroup, aoValues, asValues = None):
+ """Adds a row to the data table."""
+ if asValues:
+ self.fHasStringValues = True;
+ self.aoTable.append(WuiHlpGraphDataTable.Row(sGroup, aoValues, asValues));
+ return True;
+
+ def getGroupCount(self):
+ """Gets the number of data groups (rows)."""
+ return len(self.aoTable) - 1;
+
+
+class WuiHlpGraphDataTableEx(object): # pylint: disable=too-few-public-methods
+ """
+ Data container for an table/graph with optional error bars on the Y values.
+ """
+
+ class DataSeries(object): # pylint: disable=too-few-public-methods
+ """
+ A data series.
+
+ The aoXValues, aoYValues and aoYErrorBars are parallel arrays, making a
+ series of (X,Y,Y-err-above-delta,Y-err-below-delta) points.
+
+ The error bars are optional.
+ """
+ def __init__(self, sName, aoXValues, aoYValues, asHtmlTooltips = None, aoYErrorBarBelow = None, aoYErrorBarAbove = None):
+ self.sName = sName;
+ self.aoXValues = aoXValues;
+ self.aoYValues = aoYValues;
+ self.asHtmlTooltips = asHtmlTooltips;
+ self.aoYErrorBarBelow = aoYErrorBarBelow;
+ self.aoYErrorBarAbove = aoYErrorBarAbove;
+
+ def __init__(self, sXUnit, sYUnit):
+ self.sXUnit = sXUnit;
+ self.sYUnit = sYUnit;
+ self.aoSeries = [];
+
+ def addDataSeries(self, sName, aoXValues, aoYValues, asHtmlTooltips = None, aoYErrorBarBelow = None, aoYErrorBarAbove = None):
+ """Adds an data series to the table."""
+ self.aoSeries.append(WuiHlpGraphDataTableEx.DataSeries(sName, aoXValues, aoYValues, asHtmlTooltips,
+ aoYErrorBarBelow, aoYErrorBarAbove));
+ return True;
+
+ def getDataSeriesCount(self):
+ """Gets the number of data series."""
+ return len(self.aoSeries);
+
+
+#
+# Dynamically choose implementation.
+#
+if True: # pylint: disable=using-constant-test
+ from testmanager.webui import wuihlpgraphgooglechart as GraphImplementation;
+else:
+ try:
+ import matplotlib; # pylint: disable=unused-import,import-error,import-error,wrong-import-order
+ from testmanager.webui import wuihlpgraphmatplotlib as GraphImplementation; # pylint: disable=ungrouped-imports
+ except:
+ from testmanager.webui import wuihlpgraphsimple as GraphImplementation;
+
+# pylint: disable=invalid-name
+WuiHlpBarGraph = GraphImplementation.WuiHlpBarGraph;
+WuiHlpLineGraph = GraphImplementation.WuiHlpLineGraph;
+WuiHlpLineGraphErrorbarY = GraphImplementation.WuiHlpLineGraphErrorbarY;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py
new file mode 100755
index 00000000..7ab6c6b8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraphbase.py $
+
+"""
+Test Manager Web-UI - Graph Helpers - Base Class.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+class WuiHlpGraphBase(object):
+ """
+ Base class for the Graph helpers.
+ """
+
+ ## Set of colors that can be used by child classes to color data series.
+ kasColors = \
+ [
+ '#0000ff', # Blue
+ '#00ff00', # Green
+ '#ff0000', # Red
+ '#000000', # Black
+
+ '#00ffff', # Cyan/Aqua
+ '#ff00ff', # Magenta/Fuchsia
+ '#ffff00', # Yellow
+ '#8b4513', # SaddleBrown
+
+ '#7b68ee', # MediumSlateBlue
+ '#ffc0cb', # Pink
+ '#bdb76b', # DarkKhaki
+ '#008080', # Teal
+
+ '#bc8f8f', # RosyBrown
+ '#000080', # Navy(Blue)
+ '#dc143c', # Crimson
+ '#800080', # Purple
+
+ '#daa520', # Goldenrod
+ '#40e0d0', # Turquoise
+ '#00bfff', # DeepSkyBlue
+ '#c0c0c0', # Silver
+ ];
+
+
+ def __init__(self, sId, oData, oDisp):
+ self._sId = sId;
+ self._oData = oData;
+ self._oDisp = oDisp;
+ # Graph output dimensions.
+ self._cxGraph = 1024;
+ self._cyGraph = 448;
+ self._cDpiGraph = 96;
+ # Other graph attributes
+ self._sTitle = None;
+ self._cPtFont = 8;
+
+ def headerContent(self):
+ """
+ Returns content that goes into the HTML header.
+ """
+ return '';
+
+ def renderGraph(self):
+ """
+ Renders the graph.
+ Returning HTML.
+ """
+ return '<p>renderGraph needs to be overridden by the child class!</p>';
+
+ def setTitle(self, sTitle):
+ """ Sets the graph title. """
+ self._sTitle = sTitle;
+ return True;
+
+ def setWidth(self, cx):
+ """ Sets the graph width. """
+ self._cxGraph = cx;
+ return True;
+
+ def setHeight(self, cy):
+ """ Sets the graph height. """
+ self._cyGraph = cy;
+ return True;
+
+ def setDpi(self, cDotsPerInch):
+ """ Sets the graph DPI. """
+ self._cDpiGraph = cDotsPerInch;
+ return True;
+
+ def setFontSize(self, cPtFont):
+ """ Sets the default font size. """
+ self._cPtFont = cPtFont;
+ return True;
+
+
+ @staticmethod
+ def calcSeriesColor(iSeries):
+ """ Returns a #rrggbb color code for the given series. """
+ return WuiHlpGraphBase.kasColors[iSeries % len(WuiHlpGraphBase.kasColors)];
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py
new file mode 100755
index 00000000..b6861603
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py
@@ -0,0 +1,376 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraphgooglechart.py $
+
+"""
+Test Manager Web-UI - Graph Helpers - Implemented using Google Charts.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase;
+
+
+#*******************************************************************************
+#* Global Variables *
+#*******************************************************************************
+g_cGraphs = 0;
+
+class WuiHlpGraphGoogleChartsBase(WuiHlpGraphBase):
+ """ Base class for the Google Charts graphs. """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class WuiHlpBarGraph(WuiHlpGraphGoogleChartsBase):
+ """
+ Bar graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpGraphGoogleChartsBase.__init__(self, sId, oData, oDisp);
+ self.fpMax = None;
+ self.fpMin = 0.0;
+ self.fYInverted = False;
+
+ def setRangeMax(self, fpMax):
+ """ Sets the max range."""
+ self.fpMax = float(fpMax);
+ return None;
+
+ def invertYDirection(self):
+ """ Inverts the direction of the Y-axis direction. """
+ self.fYInverted = True;
+ return None;
+
+ def renderGraph(self):
+ aoTable = self._oData.aoTable # type: WuiHlpGraphDataTable
+
+ # Seems material (google.charts.Bar) cannot change the direction on the Y-axis,
+ # so we cannot get bars growing downwards from the top like we want for the
+ # reports. The classic charts OTOH cannot put X-axis labels on the top, but
+ # we just drop them all together instead, saving a little space.
+ fUseMaterial = False;
+
+ # Unique on load function.
+ global g_cGraphs;
+ iGraph = g_cGraphs;
+ g_cGraphs += 1;
+
+ sHtml = '<div id="%s">\n' % ( self._sId, );
+ sHtml += '<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>\n' \
+ '<script type="text/javascript">\n' \
+ 'google.charts.load("current", { packages: ["corechart", "bar"] });\n' \
+ 'google.setOnLoadCallback(tmDrawBarGraph%u);\n' \
+ 'function tmDrawBarGraph%u()\n' \
+ '{\n' \
+ ' var oGraph;\n' \
+ ' var dGraphOptions = \n' \
+ ' {\n' \
+ ' "title": "%s",\n' \
+ ' "hAxis": {\n' \
+ ' "title": "%s",\n' \
+ ' },\n' \
+ ' "vAxis": {\n' \
+ ' "direction": %s,\n' \
+ ' },\n' \
+ % ( iGraph,
+ iGraph,
+ webutils.escapeAttrJavaScriptStringDQ(self._sTitle) if self._sTitle is not None else '',
+ webutils.escapeAttrJavaScriptStringDQ(aoTable[0].sName) if aoTable and aoTable[0].sName else '',
+ '-1' if self.fYInverted else '1',
+ );
+ if fUseMaterial and self.fYInverted:
+ sHtml += ' "axes": { "x": { 0: { "side": "top" } }, "y": { "0": { "direction": -1, }, }, },\n';
+ sHtml += ' };\n';
+
+ # The data.
+ if self._oData.fHasStringValues and len(aoTable) > 1:
+ sHtml += ' var oData = new google.visualization.DataTable();\n';
+ # Column definitions.
+ sHtml += ' oData.addColumn("string", "%s");\n' \
+ % (webutils.escapeAttrJavaScriptStringDQ(aoTable[0].sName) if aoTable[0].sName else '',);
+ for iValue, oValue in enumerate(aoTable[0].aoValues):
+ oSampleValue = aoTable[1].aoValues[iValue];
+ if utils.isString(oSampleValue):
+ sHtml += ' oData.addColumn("string", "%s");\n' % (webutils.escapeAttrJavaScriptStringDQ(oValue),);
+ else:
+ sHtml += ' oData.addColumn("number", "%s");\n' % (webutils.escapeAttrJavaScriptStringDQ(oValue),);
+ sHtml += ' oData.addColumn({type: "string", role: "annotation"});\n';
+ # The data rows.
+ sHtml += ' oData.addRows([\n';
+ for oRow in aoTable[1:]:
+ if oRow.sName:
+ sRow = ' [ "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.sName),);
+ else:
+ sRow = ' [ null';
+ for iValue, oValue in enumerate(oRow.aoValues):
+ if not utils.isString(oValue):
+ sRow += ', %s' % (oValue,);
+ else:
+ sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oValue),);
+ if oRow.asValues[iValue]:
+ sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.asValues[iValue]),);
+ else:
+ sRow += ', null';
+ sHtml += sRow + '],\n';
+ sHtml += ' ]);\n';
+ else:
+ sHtml += ' var oData = google.visualization.arrayToDataTable([\n';
+ for oRow in aoTable:
+ sRow = ' [ "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.sName),);
+ for oValue in oRow.aoValues:
+ if utils.isString(oValue):
+ sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oValue),);
+ else:
+ sRow += ', %s' % (oValue,);
+ sHtml += sRow + '],\n';
+ sHtml += ' ]);\n';
+
+ # Create and draw.
+ if not fUseMaterial:
+ sHtml += ' oGraph = new google.visualization.ColumnChart(document.getElementById("%s"));\n' \
+ ' oGraph.draw(oData, dGraphOptions);\n' \
+ % ( self._sId, );
+ else:
+ sHtml += ' oGraph = new google.charts.Bar(document.getElementById("%s"));\n' \
+ ' oGraph.draw(oData, google.charts.Bar.convertOptions(dGraphOptions));\n' \
+ % ( self._sId, );
+
+ # clean and return.
+ sHtml += ' oData = null;\n' \
+ ' return true;\n' \
+ '};\n';
+
+ sHtml += '</script>\n' \
+ '</div>\n';
+ return sHtml;
+
+
+class WuiHlpLineGraph(WuiHlpGraphGoogleChartsBase):
+ """
+ Line graph.
+ """
+
+ ## @todo implement error bars.
+ kfNoErrorBarsSupport = True;
+
+ def __init__(self, sId, oData, oDisp = None, fErrorBarY = False):
+ # oData must be a WuiHlpGraphDataTableEx like object.
+ WuiHlpGraphGoogleChartsBase.__init__(self, sId, oData, oDisp);
+ self._cMaxErrorBars = 12;
+ self._fErrorBarY = fErrorBarY;
+
+ def setErrorBarY(self, fEnable):
+ """ Enables or Disables error bars, making this work like a line graph. """
+ self._fErrorBarY = fEnable;
+ return True;
+
+ def renderGraph(self): # pylint: disable=too-many-locals
+ fSlideFilter = True;
+
+ # Tooltips?
+ cTooltips = 0;
+ for oSeries in self._oData.aoSeries:
+ cTooltips += oSeries.asHtmlTooltips is not None;
+
+ # Unique on load function.
+ global g_cGraphs;
+ iGraph = g_cGraphs;
+ g_cGraphs += 1;
+
+ sHtml = '<div id="%s">\n' % ( self._sId, );
+ if fSlideFilter:
+ sHtml += ' <table><tr><td><div id="%s_graph"/></td></tr><tr><td><div id="%s_filter"/></td></tr></table>\n' \
+ % ( self._sId, self._sId, );
+
+ sHtml += '<script type="text/javascript" src="https://www.google.com/jsapi"></script>\n' \
+ '<script type="text/javascript">\n' \
+ 'google.load("visualization", "1.0", { packages: ["corechart"%s] });\n' \
+ 'google.setOnLoadCallback(tmDrawLineGraph%u);\n' \
+ 'function tmDrawLineGraph%u()\n' \
+ '{\n' \
+ ' var fnResize;\n' \
+ ' var fnRedraw;\n' \
+ ' var idRedrawTimer = null;\n' \
+ ' var cxCur = getElementWidthById("%s") - 20;\n' \
+ ' var oGraph;\n' \
+ ' var oData = new google.visualization.DataTable();\n' \
+ ' var fpXYRatio = %u / %u;\n' \
+ ' var dGraphOptions = \n' \
+ ' {\n' \
+ ' "title": "%s",\n' \
+ ' "width": cxCur,\n' \
+ ' "height": Math.round(cxCur / fpXYRatio),\n' \
+ ' "pointSize": 2,\n' \
+ ' "fontSize": %u,\n' \
+ ' "hAxis": { "title": "%s", "minorGridlines": { count: 5 }},\n' \
+ ' "vAxis": { "title": "%s", "minorGridlines": { count: 5 }},\n' \
+ ' "theme": "maximized",\n' \
+ ' "tooltip": { "isHtml": %s }\n' \
+ ' };\n' \
+ % ( ', "controls"' if fSlideFilter else '',
+ iGraph,
+ iGraph,
+ self._sId,
+ self._cxGraph, self._cyGraph,
+ self._sTitle if self._sTitle is not None else '',
+ self._cPtFont * self._cDpiGraph / 72, # fudge
+ self._oData.sXUnit if self._oData.sXUnit else '',
+ self._oData.sYUnit if self._oData.sYUnit else '',
+ 'true' if cTooltips > 0 else 'false',
+ );
+ if fSlideFilter:
+ sHtml += ' var oDashboard = new google.visualization.Dashboard(document.getElementById("%s"));\n' \
+ ' var oSlide = new google.visualization.ControlWrapper({\n' \
+ ' "controlType": "NumberRangeFilter",\n' \
+ ' "containerId": "%s_filter",\n' \
+ ' "options": {\n' \
+ ' "filterColumnIndex": 0,\n' \
+ ' "ui": { "width": getElementWidthById("%s") / 2 }, \n' \
+ ' }\n' \
+ ' });\n' \
+ % ( self._sId,
+ self._sId,
+ self._sId,);
+
+ # Data variables.
+ for iSeries, oSeries in enumerate(self._oData.aoSeries):
+ sHtml += ' var aSeries%u = [\n' % (iSeries,);
+ if oSeries.asHtmlTooltips is None:
+ sHtml += '[%s,%s]' % ( oSeries.aoXValues[0], oSeries.aoYValues[0],);
+ for i in range(1, len(oSeries.aoXValues)):
+ if (i & 16) == 0: sHtml += '\n';
+ sHtml += ',[%s,%s]' % ( oSeries.aoXValues[i], oSeries.aoYValues[i], );
+ else:
+ sHtml += '[%s,%s,"%s"]' \
+ % ( oSeries.aoXValues[0], oSeries.aoYValues[0],
+ webutils.escapeAttrJavaScriptStringDQ(oSeries.asHtmlTooltips[0]),);
+ for i in range(1, len(oSeries.aoXValues)):
+ if (i & 16) == 0: sHtml += '\n';
+ sHtml += ',[%s,%s,"%s"]' \
+ % ( oSeries.aoXValues[i], oSeries.aoYValues[i],
+ webutils.escapeAttrJavaScriptStringDQ(oSeries.asHtmlTooltips[i]),);
+
+ sHtml += '];\n'
+
+ sHtml += ' oData.addColumn("number", "%s");\n' % (self._oData.sXUnit if self._oData.sXUnit else '',);
+ cVColumns = 0;
+ for oSeries in self._oData.aoSeries:
+ sHtml += ' oData.addColumn("number", "%s");\n' % (oSeries.sName,);
+ if oSeries.asHtmlTooltips:
+ sHtml += ' oData.addColumn({"type": "string", "role": "tooltip", "p": {"html": true}});\n';
+ cVColumns += 1;
+ cVColumns += 1;
+ sHtml += 'var i;\n'
+
+ cVColumsDone = 0;
+ for iSeries, oSeries in enumerate(self._oData.aoSeries):
+ sVar = 'aSeries%u' % (iSeries,);
+ sHtml += ' for (i = 0; i < %s.length; i++)\n' \
+ ' {\n' \
+ ' oData.addRow([%s[i][0]%s,%s[i][1]%s%s]);\n' \
+ % ( sVar,
+ sVar,
+ ',null' * cVColumsDone,
+ sVar,
+ '' if oSeries.asHtmlTooltips is None else ',%s[i][2]' % (sVar,),
+ ',null' * (cVColumns - cVColumsDone - 1 - (oSeries.asHtmlTooltips is not None)),
+ );
+ sHtml += ' }\n' \
+ ' %s = null\n' \
+ % (sVar,);
+ cVColumsDone += 1 + (oSeries.asHtmlTooltips is not None);
+
+ # Create and draw.
+ if fSlideFilter:
+ sHtml += ' oGraph = new google.visualization.ChartWrapper({\n' \
+ ' "chartType": "LineChart",\n' \
+ ' "containerId": "%s_graph",\n' \
+ ' "options": dGraphOptions\n' \
+ ' });\n' \
+ ' oDashboard.bind(oSlide, oGraph);\n' \
+ ' oDashboard.draw(oData);\n' \
+ % ( self._sId, );
+ else:
+ sHtml += ' oGraph = new google.visualization.LineChart(document.getElementById("%s"));\n' \
+ ' oGraph.draw(oData, dGraphOptions);\n' \
+ % ( self._sId, );
+
+ # Register a resize handler for redrawing the graph, using a timer to delay it.
+ sHtml += ' fnRedraw = function() {\n' \
+ ' var cxNew = getElementWidthById("%s") - 6;\n' \
+ ' if (Math.abs(cxNew - cxCur) > 8)\n' \
+ ' {\n' \
+ ' cxCur = cxNew;\n' \
+ ' dGraphOptions["width"] = cxNew;\n' \
+ ' dGraphOptions["height"] = Math.round(cxNew / fpXYRatio);\n' \
+ ' oGraph.draw(oData, dGraphOptions);\n' \
+ ' }\n' \
+ ' clearTimeout(idRedrawTimer);\n' \
+ ' idRedrawTimer = null;\n' \
+ ' return true;\n' \
+ ' };\n' \
+ ' fnResize = function() {\n' \
+ ' if (idRedrawTimer != null) { clearTimeout(idRedrawTimer); } \n' \
+ ' idRedrawTimer = setTimeout(fnRedraw, 512);\n' \
+ ' return true;\n' \
+ ' };\n' \
+ ' if (window.attachEvent)\n' \
+ ' { window.attachEvent("onresize", fnResize); }\n' \
+ ' else if (window.addEventListener)\n' \
+ ' { window.addEventListener("resize", fnResize, true); }\n' \
+ % ( self._sId, );
+
+ # clean up what the callbacks don't need.
+ sHtml += ' oData = null;\n' \
+ ' aaaSeries = null;\n';
+
+ # done;
+ sHtml += ' return true;\n' \
+ '};\n';
+
+ sHtml += '</script>\n' \
+ '</div>\n';
+ return sHtml;
+
+
+class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph):
+ """
+ Line graph with an errorbar for the Y axis.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpLineGraph.__init__(self, sId, oData, fErrorBarY = True);
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py
new file mode 100755
index 00000000..cb5cf893
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py
@@ -0,0 +1,341 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraphmatplotlib.py $
+
+"""
+Test Manager Web-UI - Graph Helpers - Implemented using matplotlib.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard Python Import and extensions installed on the system.
+import re;
+import sys;
+if sys.version_info[0] >= 3:
+ from io import StringIO as StringIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias
+else:
+ from StringIO import StringIO as StringIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias
+
+import matplotlib; # pylint: disable=import-error
+matplotlib.use('Agg'); # Force backend.
+import matplotlib.pyplot; # pylint: disable=import-error
+from numpy import arange as numpy_arange; # pylint: disable=no-name-in-module,import-error,wrong-import-order
+
+# Validation Kit imports.
+from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase;
+
+
+class WuiHlpGraphMatplotlibBase(WuiHlpGraphBase):
+ """ Base class for the matplotlib graphs. """
+
+ def __init__(self, sId, oData, oDisp):
+ WuiHlpGraphBase.__init__(self, sId, oData, oDisp);
+ self._fXkcdStyle = True;
+
+ def setXkcdStyle(self, fEnabled = True):
+ """ Enables xkcd style graphs for implementations that supports it. """
+ self._fXkcdStyle = fEnabled;
+ return True;
+
+ def _createFigure(self):
+ """
+ Wrapper around matplotlib.pyplot.figure that feeds the figure the
+ basic graph configuration.
+ """
+ if self._fXkcdStyle and matplotlib.__version__ > '1.2.9':
+ matplotlib.pyplot.xkcd(); # pylint: disable=no-member
+ matplotlib.rcParams.update({'font.size': self._cPtFont});
+
+ oFigure = matplotlib.pyplot.figure(figsize = (float(self._cxGraph) / self._cDpiGraph,
+ float(self._cyGraph) / self._cDpiGraph),
+ dpi = self._cDpiGraph);
+ return oFigure;
+
+ def _produceSvg(self, oFigure, fTightLayout = True):
+ """ Creates an SVG string from the given figure. """
+ oOutput = StringIO();
+ if fTightLayout:
+ oFigure.tight_layout();
+ oFigure.savefig(oOutput, format = 'svg');
+
+ if self._oDisp and self._oDisp.isBrowserGecko('20100101'):
+ # This browser will stretch images to fit if no size or width is given.
+ sSubstitute = r'\1 \3 reserveAspectRatio="xMidYMin meet"';
+ else:
+ # Chrome and IE likes to have the sizes as well as the viewBox.
+ sSubstitute = r'\1 \3 reserveAspectRatio="xMidYMin meet" \2 \4';
+ return re.sub(r'(<svg) (height="\d+pt") (version="\d+.\d+" viewBox="\d+ \d+ \d+ \d+") (width="\d+pt")',
+ sSubstitute,
+ oOutput.getvalue().decode('utf8'),
+ count = 1);
+
+class WuiHlpBarGraph(WuiHlpGraphMatplotlibBase):
+ """
+ Bar graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp);
+ self.fpMax = None;
+ self.fpMin = 0.0;
+ self.cxBarWidth = None;
+
+ def setRangeMax(self, fpMax):
+ """ Sets the max range."""
+ self.fpMax = float(fpMax);
+ return None;
+
+ def invertYDirection(self):
+ """ Inverts the direction of the Y-axis direction. """
+ ## @todo self.fYInverted = True;
+ return None;
+
+ def renderGraph(self): # pylint: disable=too-many-locals
+ aoTable = self._oData.aoTable;
+
+ #
+ # Extract/structure the required data.
+ #
+ aoSeries = [];
+ for j in range(len(aoTable[1].aoValues)):
+ aoSeries.append([]);
+ asNames = [];
+ oXRange = numpy_arange(self._oData.getGroupCount());
+ fpMin = self.fpMin;
+ fpMax = self.fpMax;
+ if self.fpMax is None:
+ fpMax = float(aoTable[1].aoValues[0]);
+
+ for i in range(1, len(aoTable)):
+ asNames.append(aoTable[i].sName);
+ for j, oValue in enumerate(aoTable[i].aoValues):
+ fpValue = float(oValue);
+ aoSeries[j].append(fpValue);
+ if fpValue < fpMin:
+ fpMin = fpValue;
+ if fpValue > fpMax:
+ fpMax = fpValue;
+
+ fpMid = fpMin + (fpMax - fpMin) / 2.0;
+
+ if self.cxBarWidth is None:
+ self.cxBarWidth = 1.0 / (len(aoTable[0].asValues) + 1.1);
+
+ # Render the PNG.
+ oFigure = self._createFigure();
+ oSubPlot = oFigure.add_subplot(1, 1, 1);
+
+ aoBars = [];
+ for i, _ in enumerate(aoSeries):
+ sColor = self.calcSeriesColor(i);
+ aoBars.append(oSubPlot.bar(oXRange + self.cxBarWidth * i,
+ aoSeries[i],
+ self.cxBarWidth,
+ color = sColor,
+ align = 'edge'));
+
+ #oSubPlot.set_title('Title')
+ #oSubPlot.set_xlabel('X-axis')
+ #oSubPlot.set_xticks(oXRange + self.cxBarWidth);
+ oSubPlot.set_xticks(oXRange);
+ oLegend = oSubPlot.legend(aoTable[0].asValues, loc = 'best', fancybox = True);
+ oLegend.get_frame().set_alpha(0.5);
+ oSubPlot.set_xticklabels(asNames, ha = "left");
+ #oSubPlot.set_ylabel('Y-axis')
+ oSubPlot.set_yticks(numpy_arange(fpMin, fpMax + (fpMax - fpMin) / 10 * 0, fpMax / 10));
+ oSubPlot.grid(True);
+ fpPadding = (fpMax - fpMin) * 0.02;
+ for i, _ in enumerate(aoBars):
+ aoRects = aoBars[i]
+ for j, _ in enumerate(aoRects):
+ oRect = aoRects[j];
+ fpValue = float(aoTable[j + 1].aoValues[i]);
+ if fpValue <= fpMid:
+ oSubPlot.text(oRect.get_x() + oRect.get_width() / 2.0,
+ oRect.get_height() + fpPadding,
+ aoTable[j + 1].asValues[i],
+ ha = 'center', va = 'bottom', rotation = 'vertical', alpha = 0.6, fontsize = 'small');
+ else:
+ oSubPlot.text(oRect.get_x() + oRect.get_width() / 2.0,
+ oRect.get_height() - fpPadding,
+ aoTable[j + 1].asValues[i],
+ ha = 'center', va = 'top', rotation = 'vertical', alpha = 0.6, fontsize = 'small');
+
+ return self._produceSvg(oFigure);
+
+
+
+
+class WuiHlpLineGraph(WuiHlpGraphMatplotlibBase):
+ """
+ Line graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None, fErrorBarY = False):
+ # oData must be a WuiHlpGraphDataTableEx like object.
+ WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp);
+ self._cMaxErrorBars = 12;
+ self._fErrorBarY = fErrorBarY;
+
+ def setErrorBarY(self, fEnable):
+ """ Enables or Disables error bars, making this work like a line graph. """
+ self._fErrorBarY = fEnable;
+ return True;
+
+ def renderGraph(self): # pylint: disable=too-many-locals
+ aoSeries = self._oData.aoSeries;
+
+ oFigure = self._createFigure();
+ oSubPlot = oFigure.add_subplot(1, 1, 1);
+ if self._oData.sYUnit is not None:
+ oSubPlot.set_ylabel(self._oData.sYUnit);
+ if self._oData.sXUnit is not None:
+ oSubPlot.set_xlabel(self._oData.sXUnit);
+
+ cSeriesNames = 0;
+ cYMin = 1000;
+ cYMax = 0;
+ for iSeries, oSeries in enumerate(aoSeries):
+ sColor = self.calcSeriesColor(iSeries);
+ cYMin = min(cYMin, min(oSeries.aoYValues));
+ cYMax = max(cYMax, max(oSeries.aoYValues));
+ if not self._fErrorBarY:
+ oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues, color = sColor);
+ elif len(oSeries.aoXValues) > self._cMaxErrorBars:
+ if matplotlib.__version__ < '1.3.0':
+ oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues, color = sColor);
+ else:
+ oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues,
+ yerr = [oSeries.aoYErrorBarBelow, oSeries.aoYErrorBarAbove],
+ errorevery = len(oSeries.aoXValues) / self._cMaxErrorBars,
+ color = sColor );
+ else:
+ oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues,
+ yerr = [oSeries.aoYErrorBarBelow, oSeries.aoYErrorBarAbove],
+ color = sColor);
+ cSeriesNames += oSeries.sName is not None;
+
+ if cYMin != 0 or cYMax != 0:
+ oSubPlot.set_ylim(bottom = 0);
+
+ if cSeriesNames > 0:
+ oLegend = oSubPlot.legend([oSeries.sName for oSeries in aoSeries], loc = 'best', fancybox = True);
+ oLegend.get_frame().set_alpha(0.5);
+
+ if self._sTitle is not None:
+ oSubPlot.set_title(self._sTitle);
+
+ if self._cxGraph >= 256:
+ oSubPlot.minorticks_on();
+ oSubPlot.grid(True, 'major', axis = 'both');
+ oSubPlot.grid(True, 'both', axis = 'x');
+
+ if True: # pylint: disable=using-constant-test
+ # oSubPlot.axis('off');
+ #oSubPlot.grid(True, 'major', axis = 'none');
+ #oSubPlot.grid(True, 'both', axis = 'none');
+ matplotlib.pyplot.setp(oSubPlot, xticks = [], yticks = []);
+
+ return self._produceSvg(oFigure);
+
+
+class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph):
+ """
+ Line graph with an errorbar for the Y axis.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpLineGraph.__init__(self, sId, oData, fErrorBarY = True);
+
+
+class WuiHlpMiniSuccessRateGraph(WuiHlpGraphMatplotlibBase):
+ """
+ Mini rate graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ """
+ oData must be a WuiHlpGraphDataTableEx like object, but only aoSeries,
+ aoSeries[].aoXValues, and aoSeries[].aoYValues will be used. The
+ values are expected to be a percentage, i.e. values between 0 and 100.
+ """
+ WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp);
+ self.setFontSize(6);
+
+ def renderGraph(self): # pylint: disable=too-many-locals
+ assert len(self._oData.aoSeries) == 1;
+ oSeries = self._oData.aoSeries[0];
+
+ # hacking
+ #self.setWidth(512);
+ #self.setHeight(128);
+ # end
+
+ oFigure = self._createFigure();
+ from mpl_toolkits.axes_grid.axislines import SubplotZero; # pylint: disable=import-error
+ oAxis = SubplotZero(oFigure, 111);
+ oFigure.add_subplot(oAxis);
+
+ # Disable all the normal axis.
+ oAxis.axis['right'].set_visible(False)
+ oAxis.axis['top'].set_visible(False)
+ oAxis.axis['bottom'].set_visible(False)
+ oAxis.axis['left'].set_visible(False)
+
+ # Use the zero axis instead.
+ oAxis.axis['yzero'].set_axisline_style('-|>');
+ oAxis.axis['yzero'].set_visible(True);
+ oAxis.axis['xzero'].set_axisline_style('-|>');
+ oAxis.axis['xzero'].set_visible(True);
+
+ if oSeries.aoYValues[-1] == 100:
+ sColor = 'green';
+ elif oSeries.aoYValues[-1] > 75:
+ sColor = 'yellow';
+ else:
+ sColor = 'red';
+ oAxis.plot(oSeries.aoXValues, oSeries.aoYValues, '.-', color = sColor, linewidth = 3);
+ oAxis.fill_between(oSeries.aoXValues, oSeries.aoYValues, facecolor = sColor, alpha = 0.5)
+
+ oAxis.set_xlim(left = -0.01);
+ oAxis.set_xticklabels([]);
+ oAxis.set_xmargin(1);
+
+ oAxis.set_ylim(bottom = 0, top = 100);
+ oAxis.set_yticks([0, 50, 100]);
+ oAxis.set_ylabel('%');
+ #oAxis.set_yticklabels([]);
+ oAxis.set_yticklabels(['', '%', '']);
+
+ return self._produceSvg(oFigure, False);
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py
new file mode 100755
index 00000000..9be0b565
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraphsimple.py $
+
+"""
+Test Manager Web-UI - Graph Helpers - Simple/Stub Implementation.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from common.webutils import escapeAttr, escapeElem;
+from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase;
+
+
+
+class WuiHlpBarGraph(WuiHlpGraphBase):
+ """
+ Bar graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpGraphBase.__init__(self, sId, oData, oDisp);
+ self.cxMaxBar = 480;
+ self.fpMax = None;
+ self.fpMin = 0.0;
+
+ def setRangeMax(self, fpMax):
+ """ Sets the max range."""
+ self.fpMax = float(fpMax);
+ return None;
+
+ def invertYDirection(self):
+ """ Not supported. """
+ return None;
+
+ def renderGraph(self):
+ aoTable = self._oData.aoTable;
+ sReport = '<div class="tmbargraph">\n';
+
+ # Figure the range.
+ fpMin = self.fpMin;
+ fpMax = self.fpMax;
+ if self.fpMax is None:
+ fpMax = float(aoTable[1].aoValues[0]);
+ for i in range(1, len(aoTable)):
+ for oValue in aoTable[i].aoValues:
+ fpValue = float(oValue);
+ if fpValue < fpMin:
+ fpMin = fpValue;
+ if fpValue > fpMax:
+ fpMax = fpValue;
+ assert fpMin >= 0;
+
+ # Format the data.
+ sReport += '<table class="tmbargraphl1" border="1" id="%s">\n' % (escapeAttr(self._sId),);
+ for i in range(1, len(aoTable)):
+ oRow = aoTable[i];
+ sReport += ' <tr>\n' \
+ ' <td>%s</td>\n' \
+ ' <td height="100%%" width="%spx">\n' \
+ ' <table class="tmbargraphl2" height="100%%" width="100%%" ' \
+ 'border="0" cellspacing="0" cellpadding="0">\n' \
+ % (escapeElem(oRow.sName), escapeAttr(str(self.cxMaxBar + 2)));
+ for j, oValue in enumerate(oRow.aoValues):
+ cPct = int(float(oValue) * 100 / fpMax);
+ cxBar = int(float(oValue) * self.cxMaxBar / fpMax);
+ sValue = escapeElem(oRow.asValues[j]);
+ sColor = self.kasColors[j % len(self.kasColors)];
+ sInvColor = 'white';
+ if sColor[0] == '#' and len(sColor) == 7:
+ sInvColor = '#%06x' % (~int(sColor[1:],16) & 0xffffff,);
+
+ sReport += ' <tr><td>\n' \
+ ' <table class="tmbargraphl3" height="100%%" border="0" cellspacing="0" cellpadding="0">\n' \
+ ' <tr>\n';
+ if cPct >= 99:
+ sReport += ' <td width="%spx" nowrap bgcolor="%s" align="right" style="color:%s;">' \
+ '%s&nbsp;</td>\n' \
+ % (cxBar, sColor, sInvColor, sValue);
+ elif cPct < 1:
+ sReport += ' <td width="%spx" nowrap style="color:%s;">%s</td>\n' \
+ % (self.cxMaxBar - cxBar, sColor, sValue);
+ elif cPct >= 50:
+ sReport += ' <td width="%spx" nowrap bgcolor="%s" align="right" style="color:%s;">' \
+ '%s&nbsp;</td>\n' \
+ ' <td width="%spx" nowrap><div>&nbsp;</div></td>\n' \
+ % (cxBar, sColor, sInvColor, sValue, self.cxMaxBar - cxBar);
+ else:
+ sReport += ' <td width="%spx" nowrap bgcolor="%s"></td>\n' \
+ ' <td width="%spx" nowrap>&nbsp;%s</td>\n' \
+ % (cxBar, sColor, self.cxMaxBar - cxBar, sValue);
+ sReport += ' </tr>\n' \
+ ' </table>\n' \
+ ' </td></tr>\n'
+ sReport += ' </table>\n' \
+ ' </td>\n' \
+ ' </tr>\n';
+ if i + 1 < len(aoTable) and len(oRow.aoValues) > 1:
+ sReport += ' <tr></tr>\n'
+
+ sReport += '</table>\n';
+
+ sReport += '<div class="tmgraphlegend">\n' \
+ ' <p>Legend:\n';
+ for j, sValue in enumerate(aoTable[0].asValues):
+ sColor = self.kasColors[j % len(self.kasColors)];
+ sReport += ' <font color="%s">&#x25A0; %s</font>\n' % (sColor, escapeElem(sValue),);
+ sReport += ' </p>\n' \
+ '</div>\n';
+
+ sReport += '</div>\n';
+ return sReport;
+
+
+
+
+class WuiHlpLineGraph(WuiHlpGraphBase):
+ """
+ Line graph.
+ """
+
+ def __init__(self, sId, oData, oDisp):
+ WuiHlpGraphBase.__init__(self, sId, oData, oDisp);
+
+
+class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph):
+ """
+ Line graph with an errorbar for the Y axis.
+ """
+
+ pass; # pylint: disable=unnecessary-pass
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py b/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py
new file mode 100755
index 00000000..45a847ed
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py
@@ -0,0 +1,251 @@
+# -*- coding: utf-8 -*-
+# $Id: wuilogviewer.py $
+
+"""
+Test Manager WUI - Log viewer
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from common import webutils;
+from testmanager.core.testset import TestSetData;
+from testmanager.webui.wuicontentbase import WuiContentBase, WuiTmLink;
+from testmanager.webui.wuimain import WuiMain;
+
+
+class WuiLogViewer(WuiContentBase):
+ """Log viewer."""
+
+ def __init__(self, oTestSet, oLogFile, cbChunk, iChunk, aoTimestamps, oDisp = None, fnDPrint = None):
+ WuiContentBase.__init__(self, oDisp = oDisp, fnDPrint = fnDPrint);
+ self._oTestSet = oTestSet;
+ self._oLogFile = oLogFile;
+ self._cbChunk = cbChunk;
+ self._iChunk = iChunk;
+ self._aoTimestamps = aoTimestamps;
+
+ def _generateNavigation(self, cbFile):
+ """Generate the HTML for the log navigation."""
+
+ dParams = {
+ WuiMain.ksParamAction: WuiMain.ksActionViewLog,
+ WuiMain.ksParamLogSetId: self._oTestSet.idTestSet,
+ WuiMain.ksParamLogFileId: self._oLogFile.idTestResultFile,
+ WuiMain.ksParamLogChunkSize: self._cbChunk,
+ WuiMain.ksParamLogChunkNo: self._iChunk,
+ };
+
+ #
+ # The page walker.
+ #
+ dParams2 = dict(dParams);
+ del dParams2[WuiMain.ksParamLogChunkNo];
+ sHrefFmt = '<a href="?%s&%s=%%s" title="%%s">%%s</a>' \
+ % (webutils.encodeUrlParams(dParams2).replace('%', '%%'), WuiMain.ksParamLogChunkNo,);
+ sHtmlWalker = self.genericPageWalker(self._iChunk, (cbFile + self._cbChunk - 1) // self._cbChunk,
+ sHrefFmt, 11, 0, 'chunk');
+
+ #
+ # The chunk size selector.
+ #
+
+ dParams2 = dict(dParams);
+ del dParams2[WuiMain.ksParamLogChunkSize];
+ sHtmlSize = '<form name="ChunkSizeForm" method="GET">\n' \
+ ' Max <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value;" title="Max items per page">\n' \
+ % ( WuiMain.ksParamLogChunkSize, webutils.encodeUrlParams(dParams2), WuiMain.ksParamLogChunkSize,);
+
+ for cbChunk in [ 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152,
+ 4194304, 8388608, 16777216 ]:
+ sHtmlSize += ' <option value="%d" %s>%d bytes</option>\n' \
+ % (cbChunk, 'selected="selected"' if cbChunk == self._cbChunk else '', cbChunk);
+ sHtmlSize += ' </select> per page\n' \
+ '</form>\n'
+
+ #
+ # Download links.
+ #
+ oRawLink = WuiTmLink('View Raw', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionGetFile,
+ WuiMain.ksParamGetFileSetId: self._oTestSet.idTestSet,
+ WuiMain.ksParamGetFileId: self._oLogFile.idTestResultFile,
+ WuiMain.ksParamGetFileDownloadIt: False,
+ },
+ sTitle = '%u MiB' % ((cbFile + 1048576 - 1) // 1048576,) );
+ oDownloadLink = WuiTmLink('Download Log', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionGetFile,
+ WuiMain.ksParamGetFileSetId: self._oTestSet.idTestSet,
+ WuiMain.ksParamGetFileId: self._oLogFile.idTestResultFile,
+ WuiMain.ksParamGetFileDownloadIt: True,
+ },
+ sTitle = '%u MiB' % ((cbFile + 1048576 - 1) // 1048576,) );
+ oTestSetLink = WuiTmLink('Test Set', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: self._oTestSet.idTestSet,
+ });
+
+
+ #
+ # Combine the elements and return.
+ #
+ return '<div class="tmlogviewernavi">\n' \
+ ' <table width=100%>\n' \
+ ' <tr>\n' \
+ ' <td width=20%>\n' \
+ ' ' + oTestSetLink.toHtml() + '\n' \
+ ' ' + oRawLink.toHtml() + '\n' \
+ ' ' + oDownloadLink.toHtml() + '\n' \
+ ' </td>\n' \
+ ' <td width=60% align=center>' + sHtmlWalker + '</td>' \
+ ' <td width=20% align=right>' + sHtmlSize + '</td>\n' \
+ ' </tr>\n' \
+ ' </table>\n' \
+ '</div>\n';
+
+ def _displayLog(self, oFile, offFile, cbFile, aoTimestamps):
+ """Displays the current section of the log file."""
+ from testmanager.core import db;
+
+ def prepCurTs():
+ """ Formats the current timestamp. """
+ if iCurTs < len(aoTimestamps):
+ oTsZulu = db.dbTimestampToZuluDatetime(aoTimestamps[iCurTs]);
+ return (oTsZulu.strftime('%H:%M:%S.%f'), oTsZulu.strftime('%H_%M_%S_%f'));
+ return ('~~|~~|~~|~~~~~~', '~~|~~|~~|~~~~~~'); # ASCII chars with high values. Limit hits.
+
+ def isCurLineAtOrAfterCurTs():
+ """ Checks if the current line starts with a timestamp that is after the current one. """
+ if len(sLine) >= 15 \
+ and sLine[2] == ':' \
+ and sLine[5] == ':' \
+ and sLine[8] == '.' \
+ and sLine[14] in '0123456789':
+ if sLine[:15] >= sCurTs and iCurTs < len(aoTimestamps):
+ return True;
+ return False;
+
+ # Figure the end offset.
+ offEnd = offFile + self._cbChunk;
+ offEnd = min(offEnd, cbFile);
+
+ #
+ # Here is an annoying thing, we cannot seek in zip file members. So,
+ # since we have to read from the start, we can just as well count line
+ # numbers while we're at it.
+ #
+ iCurTs = 0;
+ (sCurTs, sCurId) = prepCurTs();
+ offCur = 0;
+ iLine = 0;
+ while True:
+ sLine = oFile.readline().decode('utf-8', 'replace');
+ offLine = offCur;
+ iLine += 1;
+ offCur += len(sLine);
+ if offCur >= offFile or not sLine:
+ break;
+ while isCurLineAtOrAfterCurTs():
+ iCurTs += 1;
+ (sCurTs, sCurId) = prepCurTs();
+
+ #
+ # Got to where we wanted, format the chunk.
+ #
+ asLines = ['\n<div class="tmlog">\n<pre>\n', ];
+ while True:
+ # The timestamp IDs.
+ sPrevTs = '';
+ while isCurLineAtOrAfterCurTs():
+ if sPrevTs != sCurTs:
+ asLines.append('<a id="%s"></a>' % (sCurId,));
+ iCurTs += 1;
+ (sCurTs, sCurId) = prepCurTs();
+
+ # The line.
+ asLines.append('<a id="L%d" href="#L%d">%05d</a><a id="O%d"></a>%s\n' \
+ % (iLine, iLine, iLine, offLine, webutils.escapeElem(sLine.rstrip())));
+
+ # next
+ if offCur >= offEnd:
+ break;
+ sLine = oFile.readline().decode('utf-8', 'replace');
+ offLine = offCur;
+ iLine += 1;
+ offCur += len(sLine);
+ if not sLine:
+ break;
+ asLines.append('<pre/></div>\n');
+ return ''.join(asLines);
+
+
+ def show(self):
+ """Shows the log."""
+
+ if self._oLogFile.sDescription not in [ '', None ]:
+ sTitle = '%s - %s' % (self._oLogFile.sFile, self._oLogFile.sDescription);
+ else:
+ sTitle = '%s' % (self._oLogFile.sFile,);
+
+ #
+ # Open the log file. No universal line endings here.
+ #
+ (oFile, oSizeOrError, _) = self._oTestSet.openFile(self._oLogFile.sFile, 'rb');
+ if oFile is None:
+ return (sTitle, '<p>%s</p>\n' % (webutils.escapeElem(oSizeOrError),),);
+ cbFile = oSizeOrError;
+
+ #
+ # Generate the page.
+ #
+
+ # Start with a focus hack.
+ sHtml = '<div id="tmlogoutdiv" tabindex="0">\n' \
+ '<script lang="text/javascript">\n' \
+ 'document.getElementById(\'tmlogoutdiv\').focus();\n' \
+ '</script>\n';
+
+ sNaviHtml = self._generateNavigation(cbFile);
+ sHtml += sNaviHtml;
+
+ offFile = self._iChunk * self._cbChunk;
+ if offFile < cbFile:
+ sHtml += self._displayLog(oFile, offFile, cbFile, self._aoTimestamps);
+ sHtml += sNaviHtml;
+ else:
+ sHtml += '<p>End Of File</p>';
+
+ return (sTitle, sHtml);
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuimain.py b/src/VBox/ValidationKit/testmanager/webui/wuimain.py
new file mode 100755
index 00000000..3f059775
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuimain.py
@@ -0,0 +1,1344 @@
+# -*- coding: utf-8 -*-
+# $Id: wuimain.py $
+
+"""
+Test Manager Core - WUI - The Main page.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard Python imports.
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core.base import TMExceptionBase, TMTooManyRows;
+from testmanager.webui.wuibase import WuiDispatcherBase, WuiException;
+from testmanager.webui.wuicontentbase import WuiTmLink;
+from common import webutils, utils;
+
+
+
+class WuiMain(WuiDispatcherBase):
+ """
+ WUI Main page.
+
+ Note! All cylic dependency avoiance stuff goes here in the dispatcher code,
+ not in the action specific code. This keeps the uglyness in one place
+ and reduces load time dependencies in the more critical code path.
+ """
+
+ ## The name of the script.
+ ksScriptName = 'index.py'
+
+ ## @name Actions
+ ## @{
+ ksActionResultsUnGrouped = 'ResultsUnGrouped'
+ ksActionResultsGroupedBySchedGroup = 'ResultsGroupedBySchedGroup'
+ ksActionResultsGroupedByTestGroup = 'ResultsGroupedByTestGroup'
+ ksActionResultsGroupedByBuildRev = 'ResultsGroupedByBuildRev'
+ ksActionResultsGroupedByBuildCat = 'ResultsGroupedByBuildCat'
+ ksActionResultsGroupedByTestBox = 'ResultsGroupedByTestBox'
+ ksActionResultsGroupedByTestCase = 'ResultsGroupedByTestCase'
+ ksActionResultsGroupedByOS = 'ResultsGroupedByOS'
+ ksActionResultsGroupedByArch = 'ResultsGroupedByArch'
+ ksActionTestSetDetails = 'TestSetDetails';
+ ksActionTestResultDetails = ksActionTestSetDetails;
+ ksActionTestSetDetailsFromResult = 'TestSetDetailsFromResult'
+ ksActionTestResultFailureDetails = 'TestResultFailureDetails'
+ ksActionTestResultFailureAdd = 'TestResultFailureAdd'
+ ksActionTestResultFailureAddPost = 'TestResultFailureAddPost'
+ ksActionTestResultFailureEdit = 'TestResultFailureEdit'
+ ksActionTestResultFailureEditPost = 'TestResultFailureEditPost'
+ ksActionTestResultFailureDoRemove = 'TestResultFailureDoRemove'
+ ksActionViewLog = 'ViewLog'
+ ksActionGetFile = 'GetFile'
+ ksActionReportSummary = 'ReportSummary';
+ ksActionReportRate = 'ReportRate';
+ ksActionReportTestCaseFailures = 'ReportTestCaseFailures';
+ ksActionReportTestBoxFailures = 'ReportTestBoxFailures';
+ ksActionReportFailureReasons = 'ReportFailureReasons';
+ ksActionGraphWiz = 'GraphWiz';
+ ksActionVcsHistoryTooltip = 'VcsHistoryTooltip'; ##< Hardcoded in common.js.
+ ## @}
+
+ ## @name Standard report parameters
+ ## @{
+ ksParamReportPeriods = 'cPeriods';
+ ksParamReportPeriodInHours = 'cHoursPerPeriod';
+ ksParamReportSubject = 'sSubject';
+ ksParamReportSubjectIds = 'SubjectIds';
+ ## @}
+
+ ## @name Graph Wizard parameters
+ ## Common parameters: ksParamReportPeriods, ksParamReportPeriodInHours, ksParamReportSubjectIds,
+ ## ksParamReportSubject, ksParamEffectivePeriod, and ksParamEffectiveDate.
+ ## @{
+ ksParamGraphWizTestBoxIds = 'aidTestBoxes';
+ ksParamGraphWizBuildCatIds = 'aidBuildCats';
+ ksParamGraphWizTestCaseIds = 'aidTestCases';
+ ksParamGraphWizSepTestVars = 'fSepTestVars';
+ ksParamGraphWizImpl = 'enmImpl';
+ ksParamGraphWizWidth = 'cx';
+ ksParamGraphWizHeight = 'cy';
+ ksParamGraphWizDpi = 'dpi';
+ ksParamGraphWizFontSize = 'cPtFont';
+ ksParamGraphWizErrorBarY = 'fErrorBarY';
+ ksParamGraphWizMaxErrorBarY = 'cMaxErrorBarY';
+ ksParamGraphWizMaxPerGraph = 'cMaxPerGraph';
+ ksParamGraphWizXkcdStyle = 'fXkcdStyle';
+ ksParamGraphWizTabular = 'fTabular';
+ ksParamGraphWizSrcTestSetId = 'idSrcTestSet';
+ ## @}
+
+ ## @name Graph implementations values for ksParamGraphWizImpl.
+ ## @{
+ ksGraphWizImpl_Default = 'default';
+ ksGraphWizImpl_Matplotlib = 'matplotlib';
+ ksGraphWizImpl_Charts = 'charts';
+ kasGraphWizImplValid = [ ksGraphWizImpl_Default, ksGraphWizImpl_Matplotlib, ksGraphWizImpl_Charts];
+ kaasGraphWizImplCombo = [
+ ( ksGraphWizImpl_Default, 'Default' ),
+ ( ksGraphWizImpl_Matplotlib, 'Matplotlib (server)' ),
+ ( ksGraphWizImpl_Charts, 'Google Charts (client)'),
+ ];
+ ## @}
+
+ ## @name Log Viewer parameters.
+ ## @{
+ ksParamLogSetId = 'LogViewer_idTestSet';
+ ksParamLogFileId = 'LogViewer_idFile';
+ ksParamLogChunkSize = 'LogViewer_cbChunk';
+ ksParamLogChunkNo = 'LogViewer_iChunk';
+ ## @}
+
+ ## @name File getter parameters.
+ ## @{
+ ksParamGetFileSetId = 'GetFile_idTestSet';
+ ksParamGetFileId = 'GetFile_idFile';
+ ksParamGetFileDownloadIt = 'GetFile_fDownloadIt';
+ ## @}
+
+ ## @name VCS history parameters.
+ ## @{
+ ksParamVcsHistoryRepository = 'repo';
+ ksParamVcsHistoryRevision = 'rev';
+ ksParamVcsHistoryEntries = 'cEntries';
+ ## @}
+
+ ## @name Test result listing parameters.
+ ## @{
+ ## If this param is specified, then show only results for this member when results grouped by some parameter.
+ ksParamGroupMemberId = 'GroupMemberId'
+ ## Optional parameter for indicating whether to restrict the listing to failures only.
+ ksParamOnlyFailures = 'OnlyFailures';
+ ## The sheriff parameter for getting failures needing a reason or two assigned to them.
+ ksParamOnlyNeedingReason = 'OnlyNeedingReason';
+ ## Result listing sorting.
+ ksParamTestResultsSortBy = 'enmSortBy'
+ ## @}
+
+ ## Effective time period. one of the first column values in kaoResultPeriods.
+ ksParamEffectivePeriod = 'sEffectivePeriod'
+
+ ## Test result period values.
+ kaoResultPeriods = [
+ ( '1 hour', '1 hour', 1 ),
+ ( '2 hours', '2 hours', 2 ),
+ ( '3 hours', '3 hours', 3 ),
+ ( '6 hours', '6 hours', 6 ),
+ ( '12 hours', '12 hours', 12 ),
+
+ ( '1 day', '1 day', 24 ),
+ ( '2 days', '2 days', 48 ),
+ ( '3 days', '3 days', 72 ),
+
+ ( '1 week', '1 week', 168 ),
+ ( '2 weeks', '2 weeks', 336 ),
+ ( '3 weeks', '3 weeks', 504 ),
+
+ ( '1 month', '1 month', 31 * 24 ), # The approx hour count varies with the start date.
+ ( '2 months', '2 months', (31 + 31) * 24 ), # Using maximum values.
+ ( '3 months', '3 months', (31 + 30 + 31) * 24 ),
+
+ ( '6 months', '6 months', (31 + 31 + 30 + 31 + 30 + 31) * 24 ),
+
+ ( '1 year', '1 year', 365 * 24 ),
+ ];
+ ## The default test result period.
+ ksResultPeriodDefault = '6 hours';
+
+
+
+ def __init__(self, oSrvGlue):
+ WuiDispatcherBase.__init__(self, oSrvGlue, self.ksScriptName);
+ self._sTemplate = 'template.html'
+
+ #
+ # Populate the action dispatcher dictionary.
+ # Lambda is forbidden because of readability, speed and reducing number of imports.
+ #
+ self._dDispatch[self.ksActionResultsUnGrouped] = self._actionResultsUnGrouped;
+ self._dDispatch[self.ksActionResultsGroupedByTestGroup] = self._actionResultsGroupedByTestGroup;
+ self._dDispatch[self.ksActionResultsGroupedByBuildRev] = self._actionResultsGroupedByBuildRev;
+ self._dDispatch[self.ksActionResultsGroupedByBuildCat] = self._actionResultsGroupedByBuildCat;
+ self._dDispatch[self.ksActionResultsGroupedByTestBox] = self._actionResultsGroupedByTestBox;
+ self._dDispatch[self.ksActionResultsGroupedByTestCase] = self._actionResultsGroupedByTestCase;
+ self._dDispatch[self.ksActionResultsGroupedByOS] = self._actionResultsGroupedByOS;
+ self._dDispatch[self.ksActionResultsGroupedByArch] = self._actionResultsGroupedByArch;
+ self._dDispatch[self.ksActionResultsGroupedBySchedGroup] = self._actionResultsGroupedBySchedGroup;
+
+ self._dDispatch[self.ksActionTestSetDetails] = self._actionTestSetDetails;
+ self._dDispatch[self.ksActionTestSetDetailsFromResult] = self._actionTestSetDetailsFromResult;
+
+ self._dDispatch[self.ksActionTestResultFailureAdd] = self._actionTestResultFailureAdd;
+ self._dDispatch[self.ksActionTestResultFailureAddPost] = self._actionTestResultFailureAddPost;
+ self._dDispatch[self.ksActionTestResultFailureDetails] = self._actionTestResultFailureDetails;
+ self._dDispatch[self.ksActionTestResultFailureDoRemove] = self._actionTestResultFailureDoRemove;
+ self._dDispatch[self.ksActionTestResultFailureEdit] = self._actionTestResultFailureEdit;
+ self._dDispatch[self.ksActionTestResultFailureEditPost] = self._actionTestResultFailureEditPost;
+
+ self._dDispatch[self.ksActionViewLog] = self._actionViewLog;
+ self._dDispatch[self.ksActionGetFile] = self._actionGetFile;
+
+ self._dDispatch[self.ksActionReportSummary] = self._actionReportSummary;
+ self._dDispatch[self.ksActionReportRate] = self._actionReportRate;
+ self._dDispatch[self.ksActionReportTestCaseFailures] = self._actionReportTestCaseFailures;
+ self._dDispatch[self.ksActionReportFailureReasons] = self._actionReportFailureReasons;
+ self._dDispatch[self.ksActionGraphWiz] = self._actionGraphWiz;
+
+ self._dDispatch[self.ksActionVcsHistoryTooltip] = self._actionVcsHistoryTooltip;
+
+ # Legacy.
+ self._dDispatch['TestResultDetails'] = self._dDispatch[self.ksActionTestSetDetails];
+
+
+ #
+ # Popupate the menus.
+ #
+
+ # Additional URL parameters keeping for time navigation.
+ sExtraTimeNav = ''
+ dCurParams = oSrvGlue.getParameters()
+ if dCurParams is not None:
+ for sExtraParam in [ self.ksParamItemsPerPage, self.ksParamEffectiveDate, self.ksParamEffectivePeriod, ]:
+ if sExtraParam in dCurParams:
+ sExtraTimeNav += '&%s' % (webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]}),)
+
+ # Additional URL parameters for reports
+ sExtraReports = '';
+ if dCurParams is not None:
+ for sExtraParam in [ self.ksParamReportPeriods, self.ksParamReportPeriodInHours, self.ksParamEffectiveDate, ]:
+ if sExtraParam in dCurParams:
+ sExtraReports += '&%s' % (webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]}),)
+
+ # Shorthand to keep within margins.
+ sActUrlBase = self._sActionUrlBase;
+ sOnlyFailures = '&%s%s' % ( webutils.encodeUrlParams({self.ksParamOnlyFailures: True}), sExtraTimeNav, );
+ sSheriff = '&%s%s' % ( webutils.encodeUrlParams({self.ksParamOnlyNeedingReason: True}), sExtraTimeNav, );
+
+ self._aaoMenus = \
+ [
+ [
+ 'Sheriff', sActUrlBase + self.ksActionResultsUnGrouped + sSheriff,
+ [
+ [ 'Grouped by', None ],
+ [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sSheriff, False ],
+ [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sSheriff, False ],
+ [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sSheriff, False ],
+ [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sSheriff, False ],
+ [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sSheriff, False ],
+ [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sSheriff, False ],
+ [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sSheriff, False ],
+ [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sSheriff, False ],
+ [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sSheriff, False ],
+ ]
+ ],
+ [
+ 'Reports', sActUrlBase + self.ksActionReportSummary,
+ [
+ [ 'Summary', sActUrlBase + self.ksActionReportSummary + sExtraReports, False ],
+ [ 'Success rate', sActUrlBase + self.ksActionReportRate + sExtraReports, False ],
+ [ 'Test case failures', sActUrlBase + self.ksActionReportTestCaseFailures + sExtraReports, False ],
+ [ 'Testbox failures', sActUrlBase + self.ksActionReportTestBoxFailures + sExtraReports, False ],
+ [ 'Failure reasons', sActUrlBase + self.ksActionReportFailureReasons + sExtraReports, False ],
+ ]
+ ],
+ [
+ 'Test Results', sActUrlBase + self.ksActionResultsUnGrouped + sExtraTimeNav,
+ [
+ [ 'Grouped by', None ],
+ [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sExtraTimeNav, False ],
+ [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sExtraTimeNav, False ],
+ [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sExtraTimeNav, False ],
+ [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sExtraTimeNav, False ],
+ [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sExtraTimeNav, False ],
+ [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sExtraTimeNav, False ],
+ [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sExtraTimeNav, False ],
+ [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sExtraTimeNav, False ],
+ [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sExtraTimeNav, False ],
+ ]
+ ],
+ [
+ 'Test Failures', sActUrlBase + self.ksActionResultsUnGrouped + sOnlyFailures,
+ [
+ [ 'Grouped by', None ],
+ [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sOnlyFailures, False ],
+ [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sOnlyFailures, False ],
+ [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sOnlyFailures, False ],
+ [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sOnlyFailures, False ],
+ [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sOnlyFailures, False ],
+ [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sOnlyFailures, False ],
+ [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sOnlyFailures, False ],
+ [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sOnlyFailures, False ],
+ [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sOnlyFailures, False ],
+ ]
+ ],
+ [
+ '> Admin', 'admin.py?' + webutils.encodeUrlParams(self._dDbgParams), []
+ ],
+ ];
+
+
+ #
+ # Overriding parent methods.
+ #
+
+ def _generatePage(self):
+ """Override parent handler in order to change page title."""
+ if self._sPageTitle is not None:
+ self._sPageTitle = 'Test Results - ' + self._sPageTitle
+
+ return WuiDispatcherBase._generatePage(self)
+
+ def _actionDefault(self):
+ """Show the default admin page."""
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ self._sAction = self.ksActionResultsUnGrouped
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeNone,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _isMenuMatch(self, sMenuUrl, sActionParam):
+ if super(WuiMain, self)._isMenuMatch(sMenuUrl, sActionParam):
+ fOnlyNeedingReason = self.getBoolParam(self.ksParamOnlyNeedingReason, fDefault = False);
+ if fOnlyNeedingReason:
+ return (sMenuUrl.find(self.ksParamOnlyNeedingReason) > 0);
+ fOnlyFailures = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False);
+ return (sMenuUrl.find(self.ksParamOnlyFailures) > 0) == fOnlyFailures \
+ and sMenuUrl.find(self.ksParamOnlyNeedingReason) < 0;
+ return False;
+
+
+ #
+ # Navigation bar stuff
+ #
+
+ def _generateSortBySelector(self, dParams, sPreamble, sPostamble):
+ """
+ Generate HTML code for the sort by selector.
+ """
+ from testmanager.core.testresults import TestResultLogic;
+
+ if self.ksParamTestResultsSortBy in dParams:
+ enmResultSortBy = dParams[self.ksParamTestResultsSortBy];
+ del dParams[self.ksParamTestResultsSortBy];
+ else:
+ enmResultSortBy = TestResultLogic.ksResultsSortByRunningAndStart;
+
+ sHtmlSortBy = '<form name="TimeForm" method="GET"> Sort by\n';
+ sHtmlSortBy += sPreamble;
+ sHtmlSortBy += '\n <select name="%s" onchange="window.location=' % (self.ksParamTestResultsSortBy,);
+ sHtmlSortBy += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamTestResultsSortBy)
+ sHtmlSortBy += 'this.options[this.selectedIndex].value;" title="Sorting by">\n'
+
+ fSelected = False;
+ for enmCode, sTitle in TestResultLogic.kaasResultsSortByTitles:
+ if enmCode == enmResultSortBy:
+ fSelected = True;
+ sHtmlSortBy += ' <option value="%s"%s>%s</option>\n' \
+ % (enmCode, ' selected="selected"' if enmCode == enmResultSortBy else '', sTitle,);
+ assert fSelected;
+ sHtmlSortBy += ' </select>\n';
+ sHtmlSortBy += sPostamble;
+ sHtmlSortBy += '\n</form>\n'
+ return sHtmlSortBy;
+
+ def _generateStatusSelector(self, dParams, fOnlyFailures):
+ """
+ Generate HTML code for the status code selector. Currently very simple.
+ """
+ dParams[self.ksParamOnlyFailures] = not fOnlyFailures;
+ return WuiTmLink('Show all results' if fOnlyFailures else 'Only show failed tests', '', dParams,
+ fBracketed = False).toHtml();
+
+ def _generateTimeWalker(self, dParams, tsEffective, sCurPeriod):
+ """
+ Generates HTML code for walking back and forth in time.
+ """
+ # Have to do some math here. :-/
+ if tsEffective is None:
+ self._oDb.execute('SELECT CURRENT_TIMESTAMP - \'' + sCurPeriod + '\'::interval');
+ tsNext = None;
+ tsPrev = self._oDb.fetchOne()[0];
+ else:
+ self._oDb.execute('SELECT %s::TIMESTAMP - \'' + sCurPeriod + '\'::interval,\n'
+ ' %s::TIMESTAMP + \'' + sCurPeriod + '\'::interval',
+ (tsEffective, tsEffective,));
+ tsPrev, tsNext = self._oDb.fetchOne();
+
+ # Forget about page No when changing a period
+ if WuiDispatcherBase.ksParamPageNo in dParams:
+ del dParams[WuiDispatcherBase.ksParamPageNo]
+
+ # Format.
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = str(tsPrev);
+ sPrev = '<a href="?%s" title="One period earlier">&lt;&lt;</a>&nbsp;&nbsp;' \
+ % (webutils.encodeUrlParams(dParams),);
+
+ if tsNext is not None:
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = str(tsNext);
+ sNext = '&nbsp;&nbsp;<a href="?%s" title="One period later">&gt;&gt;</a>' \
+ % (webutils.encodeUrlParams(dParams),);
+ else:
+ sNext = '&nbsp;&nbsp;&gt;&gt;';
+
+ from testmanager.webui.wuicontentbase import WuiListContentBase; ## @todo move to better place.
+ return WuiListContentBase.generateTimeNavigation('top', self.getParameters(), self.getEffectiveDateParam(),
+ sPrev, sNext, False);
+
+ def _generateResultPeriodSelector(self, dParams, sCurPeriod):
+ """
+ Generate HTML code for result period selector.
+ """
+
+ if self.ksParamEffectivePeriod in dParams:
+ del dParams[self.ksParamEffectivePeriod];
+
+ # Forget about page No when changing a period
+ if WuiDispatcherBase.ksParamPageNo in dParams:
+ del dParams[WuiDispatcherBase.ksParamPageNo]
+
+ sHtmlPeriodSelector = '<form name="PeriodForm" method="GET">\n'
+ sHtmlPeriodSelector += ' Period is\n'
+ sHtmlPeriodSelector += ' <select name="%s" onchange="window.location=' % self.ksParamEffectivePeriod
+ sHtmlPeriodSelector += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamEffectivePeriod)
+ sHtmlPeriodSelector += 'this.options[this.selectedIndex].value;">\n'
+
+ for sPeriodValue, sPeriodCaption, _ in self.kaoResultPeriods:
+ sHtmlPeriodSelector += ' <option value="%s"%s>%s</option>\n' \
+ % (webutils.quoteUrl(sPeriodValue),
+ ' selected="selected"' if sPeriodValue == sCurPeriod else '',
+ sPeriodCaption)
+
+ sHtmlPeriodSelector += ' </select>\n' \
+ '</form>\n'
+
+ return sHtmlPeriodSelector
+
+ def _generateGroupContentSelector(self, aoGroupMembers, iCurrentMember, sAltAction):
+ """
+ Generate HTML code for group content selector.
+ """
+
+ dParams = self.getParameters()
+
+ if self.ksParamGroupMemberId in dParams:
+ del dParams[self.ksParamGroupMemberId]
+
+ if sAltAction is not None:
+ if self.ksParamAction in dParams:
+ del dParams[self.ksParamAction];
+ dParams[self.ksParamAction] = sAltAction;
+
+ sHtmlSelector = '<form name="GroupContentForm" method="GET">\n'
+ sHtmlSelector += ' <select name="%s" onchange="window.location=' % self.ksParamGroupMemberId
+ sHtmlSelector += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamGroupMemberId)
+ sHtmlSelector += 'this.options[this.selectedIndex].value;">\n'
+
+ sHtmlSelector += '<option value="-1">All</option>\n'
+
+ for iGroupMemberId, sGroupMemberName in aoGroupMembers:
+ if iGroupMemberId is not None:
+ sHtmlSelector += ' <option value="%s"%s>%s</option>\n' \
+ % (iGroupMemberId,
+ ' selected="selected"' if iGroupMemberId == iCurrentMember else '',
+ sGroupMemberName)
+
+ sHtmlSelector += ' </select>\n' \
+ '</form>\n'
+
+ return sHtmlSelector
+
+ def _generatePagesSelector(self, dParams, cItems, cItemsPerPage, iPage):
+ """
+ Generate HTML code for pages (1, 2, 3 ... N) selector
+ """
+
+ if WuiDispatcherBase.ksParamPageNo in dParams:
+ del dParams[WuiDispatcherBase.ksParamPageNo]
+
+ sHrefPtr = '<a href="?%s&%s=' % (webutils.encodeUrlParams(dParams).replace('%', '%%'),
+ WuiDispatcherBase.ksParamPageNo)
+ sHrefPtr += '%d">%s</a>'
+
+ cNumOfPages = (cItems + cItemsPerPage - 1) // cItemsPerPage;
+ cPagesToDisplay = 10
+ cPagesRangeStart = iPage - cPagesToDisplay // 2 \
+ if not iPage - cPagesToDisplay / 2 < 0 else 0
+ cPagesRangeEnd = cPagesRangeStart + cPagesToDisplay \
+ if not cPagesRangeStart + cPagesToDisplay > cNumOfPages else cNumOfPages
+ # Adjust pages range
+ if cNumOfPages < cPagesToDisplay:
+ cPagesRangeStart = 0
+ cPagesRangeEnd = cNumOfPages
+
+ # 1 2 3 4...
+ sHtmlPager = '&nbsp;\n'.join(sHrefPtr % (x, str(x + 1)) if x != iPage else str(x + 1)
+ for x in range(cPagesRangeStart, cPagesRangeEnd))
+ if cPagesRangeStart > 0:
+ sHtmlPager = '%s&nbsp; ... &nbsp;\n' % (sHrefPtr % (0, str(1))) + sHtmlPager
+ if cPagesRangeEnd < cNumOfPages:
+ sHtmlPager += ' ... %s\n' % (sHrefPtr % (cNumOfPages, str(cNumOfPages + 1)))
+
+ # Prev/Next (using << >> because &laquo; and &raquo are too tiny).
+ if iPage > 0:
+ dParams[WuiDispatcherBase.ksParamPageNo] = iPage - 1
+ sHtmlPager = ('<a title="Previous page" href="?%s">&lt;&lt;</a>&nbsp;&nbsp;\n'
+ % (webutils.encodeUrlParams(dParams), )) \
+ + sHtmlPager;
+ else:
+ sHtmlPager = '&lt;&lt;&nbsp;&nbsp;\n' + sHtmlPager
+
+ if iPage + 1 < cNumOfPages:
+ dParams[WuiDispatcherBase.ksParamPageNo] = iPage + 1
+ sHtmlPager += '\n&nbsp; <a title="Next page" href="?%s">&gt;&gt;</a>\n' % (webutils.encodeUrlParams(dParams),)
+ else:
+ sHtmlPager += '\n&nbsp; &gt;&gt;\n'
+
+ return sHtmlPager
+
+ def _generateItemPerPageSelector(self, dParams, cItemsPerPage):
+ """
+ Generate HTML code for items per page selector
+ Note! Modifies dParams!
+ """
+
+ from testmanager.webui.wuicontentbase import WuiListContentBase; ## @todo move to better place.
+ return WuiListContentBase.generateItemPerPageSelector('top', dParams, cItemsPerPage);
+
+ def _generateResultNavigation(self, cItems, cItemsPerPage, iPage, tsEffective, sCurPeriod, fOnlyFailures,
+ sHtmlMemberSelector):
+ """ Make custom time navigation bar for the results. """
+
+ # Generate the elements.
+ sHtmlStatusSelector = self._generateStatusSelector(self.getParameters(), fOnlyFailures);
+ sHtmlSortBySelector = self._generateSortBySelector(self.getParameters(), '', sHtmlStatusSelector);
+ sHtmlPeriodSelector = self._generateResultPeriodSelector(self.getParameters(), sCurPeriod)
+ sHtmlTimeWalker = self._generateTimeWalker(self.getParameters(), tsEffective, sCurPeriod);
+
+ if cItems > 0:
+ sHtmlPager = self._generatePagesSelector(self.getParameters(), cItems, cItemsPerPage, iPage)
+ sHtmlItemsPerPageSelector = self._generateItemPerPageSelector(self.getParameters(), cItemsPerPage)
+ else:
+ sHtmlPager = ''
+ sHtmlItemsPerPageSelector = ''
+
+ # Generate navigation bar
+ sHtml = '<table width=100%>\n' \
+ '<tr>\n' \
+ ' <td width=30%>' + sHtmlMemberSelector + '</td>\n' \
+ ' <td width=40% align=center>' + sHtmlTimeWalker + '</td>' \
+ ' <td width=30% align=right>\n' + sHtmlPeriodSelector + '</td>\n' \
+ '</tr>\n' \
+ '<tr>\n' \
+ ' <td width=30%>' + sHtmlSortBySelector + '</td>\n' \
+ ' <td width=40% align=center>\n' + sHtmlPager + '</td>\n' \
+ ' <td width=30% align=right>\n' + sHtmlItemsPerPageSelector + '</td>\n'\
+ '</tr>\n' \
+ '</table>\n'
+
+ return sHtml
+
+ def _generateReportNavigation(self, tsEffective, cHoursPerPeriod, cPeriods):
+ """ Make time navigation bar for the reports. """
+
+ # The period length selector.
+ dParams = self.getParameters();
+ if WuiMain.ksParamReportPeriodInHours in dParams:
+ del dParams[WuiMain.ksParamReportPeriodInHours];
+ sHtmlPeriodLength = '';
+ sHtmlPeriodLength += '<form name="ReportPeriodInHoursForm" method="GET">\n' \
+ ' Period length <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value;" title="Statistics period length in hours.">\n' \
+ % (WuiMain.ksParamReportPeriodInHours,
+ webutils.encodeUrlParams(dParams),
+ WuiMain.ksParamReportPeriodInHours)
+ for cHours in [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 18, 24, 48, 72, 96, 120, 144, 168 ]:
+ sHtmlPeriodLength += ' <option value="%d"%s>%d hour%s</option>\n' \
+ % (cHours, 'selected="selected"' if cHours == cHoursPerPeriod else '', cHours,
+ 's' if cHours > 1 else '');
+ sHtmlPeriodLength += ' </select>\n' \
+ '</form>\n'
+
+ # The period count selector.
+ dParams = self.getParameters();
+ if WuiMain.ksParamReportPeriods in dParams:
+ del dParams[WuiMain.ksParamReportPeriods];
+ sHtmlCountOfPeriods = '';
+ sHtmlCountOfPeriods += '<form name="ReportPeriodsForm" method="GET">\n' \
+ ' Periods <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value;" title="Statistics periods to report.">\n' \
+ % (WuiMain.ksParamReportPeriods,
+ webutils.encodeUrlParams(dParams),
+ WuiMain.ksParamReportPeriods)
+ for cCurPeriods in range(2, 43):
+ sHtmlCountOfPeriods += ' <option value="%d"%s>%d</option>\n' \
+ % (cCurPeriods, 'selected="selected"' if cCurPeriods == cPeriods else '', cCurPeriods);
+ sHtmlCountOfPeriods += ' </select>\n' \
+ '</form>\n'
+
+ # The time walker.
+ sHtmlTimeWalker = self._generateTimeWalker(self.getParameters(), tsEffective, '%d hours' % (cHoursPerPeriod));
+
+ # Combine them all.
+ sHtml = '<table width=100%>\n' \
+ ' <tr>\n' \
+ ' <td width=30% align="center">\n' + sHtmlPeriodLength + '</td>\n' \
+ ' <td width=40% align="center">\n' + sHtmlTimeWalker + '</td>' \
+ ' <td width=30% align="center">\n' + sHtmlCountOfPeriods + '</td>\n' \
+ ' </tr>\n' \
+ '</table>\n';
+ return sHtml;
+
+
+ #
+ # The rest of stuff
+ #
+
+ def _actionGroupedResultsListing( #pylint: disable=too-many-locals
+ self,
+ enmResultsGroupingType,
+ oResultsLogicType,
+ oResultFilterType,
+ oResultsListContentType):
+ """
+ Override generic listing action.
+
+ oResultsLogicType implements getEntriesCount, fetchResultsForListing and more.
+ oResultFilterType is a child of ModelFilterBase.
+ oResultsListContentType is a child of WuiListContentBase.
+ """
+ from testmanager.core.testresults import TestResultLogic;
+
+ cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 128);
+ iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0);
+ tsEffective = self.getEffectiveDateParam();
+ iGroupMemberId = self.getIntParam(self.ksParamGroupMemberId, iMin = -1, iMax = 999999, iDefault = -1);
+ fOnlyFailures = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False);
+ fOnlyNeedingReason = self.getBoolParam(self.ksParamOnlyNeedingReason, fDefault = False);
+ enmResultSortBy = self.getStringParam(self.ksParamTestResultsSortBy,
+ asValidValues = TestResultLogic.kasResultsSortBy,
+ sDefault = TestResultLogic.ksResultsSortByRunningAndStart);
+ oFilter = oResultFilterType().initFromParams(self);
+
+ # Get testing results period and validate it
+ asValidValues = [x for (x, _, _) in self.kaoResultPeriods]
+ sCurPeriod = self.getStringParam(self.ksParamEffectivePeriod, asValidValues = asValidValues,
+ sDefault = self.ksResultPeriodDefault)
+ assert sCurPeriod != ''; # Impossible!
+
+ self._checkForUnknownParameters()
+
+ #
+ # Fetch the group members.
+ #
+ # If no grouping is selected, we'll fill the grouping combo with
+ # testboxes just to avoid having completely useless combo box.
+ #
+ oTrLogic = TestResultLogic(self._oDb);
+ sAltSelectorAction = None;
+ if enmResultsGroupingType in (TestResultLogic.ksResultsGroupingTypeNone, TestResultLogic.ksResultsGroupingTypeTestBox,):
+ aoTmp = oTrLogic.getTestBoxes(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({(x.idTestBox, '%s (%s)' % (x.sName, str(x.ip))) for x in aoTmp }),
+ reverse = False, key = lambda asData: asData[1])
+
+ if enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestBox:
+ self._sPageTitle = 'Grouped by Test Box';
+ else:
+ self._sPageTitle = 'Ungrouped results';
+ sAltSelectorAction = self.ksActionResultsGroupedByTestBox;
+ aoGroupMembers.insert(0, [None, None]); # The "All" member.
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestGroup:
+ aoTmp = oTrLogic.getTestGroups(tsNow = tsEffective, sPeriod = sCurPeriod);
+ aoGroupMembers = sorted(list({ (x.idTestGroup, x.sName ) for x in aoTmp }),
+ reverse = False, key = lambda asData: asData[1])
+ self._sPageTitle = 'Grouped by Test Group'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeBuildRev:
+ aoTmp = oTrLogic.getBuilds(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({ (x.iRevision, '%s.%d' % (x.oCat.sBranch, x.iRevision)) for x in aoTmp }),
+ reverse = True, key = lambda asData: asData[0])
+ self._sPageTitle = 'Grouped by Build'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeBuildCat:
+ aoTmp = oTrLogic.getBuildCategories(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({ (x.idBuildCategory,
+ '%s / %s / %s / %s' % ( x.sProduct, x.sBranch, ', '.join(x.asOsArches), x.sType) )
+ for x in aoTmp }),
+ reverse = True, key = lambda asData: asData[1]);
+ self._sPageTitle = 'Grouped by Build Category'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestCase:
+ aoTmp = oTrLogic.getTestCases(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({ (x.idTestCase, '%s' % x.sName) for x in aoTmp }),
+ reverse = False, key = lambda asData: asData[1])
+ self._sPageTitle = 'Grouped by Test Case'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeOS:
+ aoTmp = oTrLogic.getOSes(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list(set(aoTmp)), reverse = False, key = lambda asData: asData[1]);
+ self._sPageTitle = 'Grouped by OS'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeArch:
+ aoTmp = oTrLogic.getArchitectures(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list(set(aoTmp)), reverse = False, key = lambda asData: asData[1]);
+ self._sPageTitle = 'Grouped by Architecture'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeSchedGroup:
+ aoTmp = oTrLogic.getSchedGroups(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({ (x.idSchedGroup, '%s' % x.sName) for x in aoTmp }),
+ reverse = False, key = lambda asData: asData[1])
+ self._sPageTitle = 'Grouped by Scheduling Group'
+
+ else:
+ raise TMExceptionBase('Unknown grouping type')
+
+ _sPageBody = ''
+ oContent = None
+ cEntriesMax = 0
+ _dParams = self.getParameters()
+ oResultLogic = oResultsLogicType(self._oDb);
+ for idMember, sMemberName in aoGroupMembers:
+ #
+ # Count and fetch entries to be displayed.
+ #
+
+ # Skip group members that were not specified.
+ if idMember != iGroupMemberId \
+ and ( (idMember is not None and enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeNone)
+ or (iGroupMemberId > 0 and enmResultsGroupingType != TestResultLogic.ksResultsGroupingTypeNone) ):
+ continue
+
+ cEntries = oResultLogic.getEntriesCount(tsNow = tsEffective,
+ sInterval = sCurPeriod,
+ oFilter = oFilter,
+ enmResultsGroupingType = enmResultsGroupingType,
+ iResultsGroupingValue = idMember,
+ fOnlyFailures = fOnlyFailures,
+ fOnlyNeedingReason = fOnlyNeedingReason);
+ if cEntries == 0: # Do not display empty groups
+ continue
+ aoEntries = oResultLogic.fetchResultsForListing(iPage * cItemsPerPage,
+ cItemsPerPage,
+ tsNow = tsEffective,
+ sInterval = sCurPeriod,
+ oFilter = oFilter,
+ enmResultSortBy = enmResultSortBy,
+ enmResultsGroupingType = enmResultsGroupingType,
+ iResultsGroupingValue = idMember,
+ fOnlyFailures = fOnlyFailures,
+ fOnlyNeedingReason = fOnlyNeedingReason);
+ cEntriesMax = max(cEntriesMax, cEntries)
+
+ #
+ # Format them.
+ #
+ oContent = oResultsListContentType(aoEntries,
+ cEntries,
+ iPage,
+ cItemsPerPage,
+ tsEffective,
+ fnDPrint = self._oSrvGlue.dprint,
+ oDisp = self)
+
+ (_, sHtml) = oContent.show(fShowNavigation = False)
+ if sMemberName is not None:
+ _sPageBody += '<table width=100%><tr><td>'
+
+ _dParams[self.ksParamGroupMemberId] = idMember
+ sLink = WuiTmLink(sMemberName, '', _dParams, fBracketed = False).toHtml()
+
+ _sPageBody += '<h2>%s (%d)</h2></td>' % (sLink, cEntries)
+ _sPageBody += '<td><br></td>'
+ _sPageBody += '</tr></table>'
+ _sPageBody += sHtml
+ _sPageBody += '<br>'
+
+ #
+ # Complete the page by slapping navigation controls at the top and
+ # bottom of it.
+ #
+ sHtmlNavigation = self._generateResultNavigation(cEntriesMax, cItemsPerPage, iPage,
+ tsEffective, sCurPeriod, fOnlyFailures,
+ self._generateGroupContentSelector(aoGroupMembers, iGroupMemberId,
+ sAltSelectorAction));
+ if cEntriesMax > 0:
+ self._sPageBody = sHtmlNavigation + _sPageBody + sHtmlNavigation;
+ else:
+ self._sPageBody = sHtmlNavigation + '<p align="center"><i>No data to display</i></p>\n';
+
+ #
+ # Now, generate a filter control panel for the side bar.
+ #
+ if hasattr(oFilter, 'kiBranches'):
+ oFilter.aCriteria[oFilter.kiBranches].fExpanded = True;
+ if hasattr(oFilter, 'kiTestStatus'):
+ oFilter.aCriteria[oFilter.kiTestStatus].fExpanded = True;
+ self._sPageFilter = self._generateResultFilter(oFilter, oResultLogic, tsEffective, sCurPeriod,
+ enmResultsGroupingType = enmResultsGroupingType,
+ aoGroupMembers = aoGroupMembers,
+ fOnlyFailures = fOnlyFailures,
+ fOnlyNeedingReason = fOnlyNeedingReason);
+ return True;
+
+ def _generateResultFilter(self, oFilter, oResultLogic, tsNow, sPeriod, enmResultsGroupingType = None, aoGroupMembers = None,
+ fOnlyFailures = False, fOnlyNeedingReason = False):
+ """
+ Generates the result filter for the left hand side.
+ """
+ _ = enmResultsGroupingType; _ = aoGroupMembers; _ = fOnlyFailures; _ = fOnlyNeedingReason;
+ oResultLogic.fetchPossibleFilterOptions(oFilter, tsNow, sPeriod)
+
+ # Add non-filter parameters as hidden fields so we can use 'GET' and have URLs to bookmark.
+ self._dSideMenuFormAttrs['method'] = 'GET';
+ sHtml = u'';
+ for sKey, oValue in self._oSrvGlue.getParameters().items():
+ if len(sKey) > 3:
+ if hasattr(oValue, 'startswith'):
+ sHtml += u'<input type="hidden" name="%s" value="%s"/>\n' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttr(oValue),);
+ else:
+ for oSubValue in oValue:
+ sHtml += u'<input type="hidden" name="%s" value="%s"/>\n' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttr(oSubValue),);
+
+ # Generate the filter panel.
+ sHtml += u'<div id="side-filters">\n' \
+ u' <p>Filters' \
+ u' <span class="tm-side-filter-title-buttons"><input type="submit" value="Apply" />\n' \
+ u' <a href="javascript:toggleSidebarSize();" class="tm-sidebar-size-link">&#x00bb;&#x00bb;</a></span></p>\n';
+ sHtml += u' <dl>\n';
+ for oCrit in oFilter.aCriteria:
+ if oCrit.aoPossible or oCrit.sType == oCrit.ksType_Ranges:
+ if ( oCrit.oSub is None \
+ and ( oCrit.sState == oCrit.ksState_Selected \
+ or (len(oCrit.aoPossible) <= 2 and oCrit.sType != oCrit.ksType_Ranges))) \
+ or ( oCrit.oSub is not None \
+ and ( oCrit.sState == oCrit.ksState_Selected \
+ or oCrit.oSub.sState == oCrit.ksState_Selected \
+ or (len(oCrit.aoPossible) <= 2 and len(oCrit.oSub.aoPossible) <= 2))) \
+ or oCrit.fExpanded is True:
+ sClass = 'sf-collapsible';
+ sChar = '&#9660;';
+ else:
+ sClass = 'sf-expandable';
+ sChar = '&#9654;';
+
+ sHtml += u' <dt class="%s"><a href="javascript:void(0)" onclick="toggleCollapsibleDtDd(this);">%s %s</a> ' \
+ % (sClass, sChar, webutils.escapeElem(oCrit.sName),);
+ sHtml += u'<span class="tm-side-filter-dt-buttons">';
+ if oCrit.sInvVarNm is not None:
+ sHtml += u'<input id="sf-union-%s" class="tm-side-filter-union-input" ' \
+ u'name="%s" value="1" type="checkbox"%s />' \
+ u'<label for="sf-union-%s" class="tm-side-filter-union-input"></label>' \
+ % ( oCrit.sInvVarNm, oCrit.sInvVarNm, ' checked' if oCrit.fInverted else '', oCrit.sInvVarNm,);
+ sHtml += u' <input type="submit" value="Apply" />';
+ sHtml += u'</span>';
+ sHtml += u'</dt>\n' \
+ u' <dd class="%s">\n' \
+ u' <ul>\n' \
+ % (sClass);
+
+ if oCrit.sType == oCrit.ksType_Ranges:
+ assert not oCrit.oSub;
+ assert not oCrit.aoPossible;
+ asValues = [];
+ for tRange in oCrit.aoSelected:
+ if tRange[0] == tRange[1]:
+ asValues.append('%s' % (tRange[0],));
+ else:
+ asValues.append('%s-%s' % (tRange[0] if tRange[0] is not None else 'inf',
+ tRange[1] if tRange[1] is not None else 'inf'));
+ sHtml += u' <li title="%s"><input type="text" name="%s" value="%s"/></li>\n' \
+ % ( webutils.escapeAttr('comma separate list of numerical ranges'), oCrit.sVarNm,
+ ', '.join(asValues), );
+ else:
+ for oDesc in oCrit.aoPossible:
+ fChecked = oDesc.oValue in oCrit.aoSelected;
+ sHtml += u' <li%s%s><label><input type="checkbox" name="%s" value="%s"%s%s/>%s%s</label>\n' \
+ % ( ' class="side-filter-irrelevant"' if oDesc.fIrrelevant else '',
+ (' title="%s"' % (webutils.escapeAttr(oDesc.sHover,)) if oDesc.sHover is not None else ''),
+ oCrit.sVarNm,
+ oDesc.oValue,
+ ' checked' if fChecked else '',
+ ' onclick="toggleCollapsibleCheckbox(this);"' if oDesc.aoSubs is not None else '',
+ webutils.escapeElem(oDesc.sDesc),
+ '<span class="side-filter-count"> [%u]</span>' % (oDesc.cTimes) if oDesc.cTimes is not None
+ else '', );
+ if oDesc.aoSubs is not None:
+ sHtml += u' <ul class="sf-checkbox-%s">\n' % ('collapsible' if fChecked else 'expandable', );
+ for oSubDesc in oDesc.aoSubs:
+ fSubChecked = oSubDesc.oValue in oCrit.oSub.aoSelected;
+ sHtml += u' <li%s%s><label><input type="checkbox" name="%s" value="%s"%s/>%s%s</label>\n' \
+ % ( ' class="side-filter-irrelevant"' if oSubDesc.fIrrelevant else '',
+ ' title="%s"' % ( webutils.escapeAttr(oSubDesc.sHover,) if oSubDesc.sHover is not None
+ else ''),
+ oCrit.oSub.sVarNm, oSubDesc.oValue, ' checked' if fSubChecked else '',
+ webutils.escapeElem(oSubDesc.sDesc),
+ '<span class="side-filter-count"> [%u]</span>' % (oSubDesc.cTimes)
+ if oSubDesc.cTimes is not None else '', );
+
+ sHtml += u' </ul>\n';
+ sHtml += u' </li>';
+
+ sHtml += u' </ul>\n' \
+ u' </dd>\n';
+
+ sHtml += u' </dl>\n';
+ sHtml += u' <input type="submit" value="Apply"/>\n';
+ sHtml += u' <input type="reset" value="Reset"/>\n';
+ sHtml += u' <button type="button" onclick="clearForm(\'side-menu-form\');">Clear</button>\n';
+ sHtml += u'</div>\n';
+ return sHtml;
+
+ def _actionResultsUnGrouped(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ #return self._actionResultsListing(TestResultLogic, WuiGroupedResultList)?
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeNone,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByTestGroup(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestGroup,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByBuildRev(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeBuildRev,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByBuildCat(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeBuildCat,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByTestBox(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestBox,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByTestCase(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestCase,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByOS(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeOS,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByArch(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeArch,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedBySchedGroup(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeSchedGroup,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+
+ def _actionTestSetDetailsCommon(self, idTestSet):
+ """Show test case execution result details."""
+ from testmanager.core.build import BuildDataEx;
+ from testmanager.core.testbox import TestBoxData;
+ from testmanager.core.testcase import TestCaseDataEx;
+ from testmanager.core.testcaseargs import TestCaseArgsDataEx;
+ from testmanager.core.testgroup import TestGroupData;
+ from testmanager.core.testresults import TestResultLogic;
+ from testmanager.core.testset import TestSetData;
+ from testmanager.webui.wuitestresult import WuiTestResult;
+
+ self._sTemplate = 'template-details.html';
+ self._checkForUnknownParameters()
+
+ oTestSetData = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ try:
+ (oTestResultTree, _) = TestResultLogic(self._oDb).fetchResultTree(idTestSet);
+ except TMTooManyRows:
+ (oTestResultTree, _) = TestResultLogic(self._oDb).fetchResultTree(idTestSet, 2);
+ oBuildDataEx = BuildDataEx().initFromDbWithId(self._oDb, oTestSetData.idBuild, oTestSetData.tsCreated);
+ try: oBuildValidationKitDataEx = BuildDataEx().initFromDbWithId(self._oDb, oTestSetData.idBuildTestSuite,
+ oTestSetData.tsCreated);
+ except: oBuildValidationKitDataEx = None;
+ oTestBoxData = TestBoxData().initFromDbWithGenId(self._oDb, oTestSetData.idGenTestBox);
+ oTestGroupData = TestGroupData().initFromDbWithId(self._oDb, ## @todo This bogus time wise. Bad DB design?
+ oTestSetData.idTestGroup, oTestSetData.tsCreated);
+ oTestCaseDataEx = TestCaseDataEx().initFromDbWithGenId(self._oDb, oTestSetData.idGenTestCase,
+ oTestSetData.tsConfig);
+ oTestCaseArgsDataEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(self._oDb, oTestSetData.idGenTestCaseArgs,
+ oTestSetData.tsConfig);
+
+ oContent = WuiTestResult(oDisp = self, fnDPrint = self._oSrvGlue.dprint);
+ (self._sPageTitle, self._sPageBody) = oContent.showTestCaseResultDetails(oTestResultTree,
+ oTestSetData,
+ oBuildDataEx,
+ oBuildValidationKitDataEx,
+ oTestBoxData,
+ oTestGroupData,
+ oTestCaseDataEx,
+ oTestCaseArgsDataEx);
+ return True
+
+ def _actionTestSetDetails(self):
+ """Show test case execution result details."""
+ from testmanager.core.testset import TestSetData;
+
+ idTestSet = self.getIntParam(TestSetData.ksParam_idTestSet);
+ return self._actionTestSetDetailsCommon(idTestSet);
+
+ def _actionTestSetDetailsFromResult(self):
+ """Show test case execution result details."""
+ from testmanager.core.testresults import TestResultData;
+ from testmanager.core.testset import TestSetData;
+ idTestResult = self.getIntParam(TestSetData.ksParam_idTestResult);
+ oTestResultData = TestResultData().initFromDbWithId(self._oDb, idTestResult);
+ return self._actionTestSetDetailsCommon(oTestResultData.idTestSet);
+
+
+ def _actionTestResultFailureAdd(self):
+ """ Pro forma. """
+ from testmanager.core.testresultfailures import TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ return self._actionGenericFormAdd(TestResultFailureData, WuiTestResultFailure);
+
+ def _actionTestResultFailureAddPost(self):
+ """Add test result failure result"""
+ from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ if self.ksParamRedirectTo not in self._dParams:
+ raise WuiException('Missing parameter ' + self.ksParamRedirectTo);
+
+ return self._actionGenericFormAddPost(TestResultFailureData, TestResultFailureLogic,
+ WuiTestResultFailure, self.ksActionResultsUnGrouped);
+
+ def _actionTestResultFailureDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.testresultfailures import TestResultFailureData, TestResultFailureLogic;
+ return self._actionGenericDoRemove(TestResultFailureLogic, TestResultFailureData.ksParam_idTestResult,
+ self.ksActionResultsUnGrouped);
+
+ def _actionTestResultFailureDetails(self):
+ """ Pro forma. """
+ from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ return self._actionGenericFormDetails(TestResultFailureData, TestResultFailureLogic,
+ WuiTestResultFailure, 'idTestResult');
+
+ def _actionTestResultFailureEdit(self):
+ """ Pro forma. """
+ from testmanager.core.testresultfailures import TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ return self._actionGenericFormEdit(TestResultFailureData, WuiTestResultFailure,
+ TestResultFailureData.ksParam_idTestResult);
+
+ def _actionTestResultFailureEditPost(self):
+ """Edit test result failure result"""
+ from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ return self._actionGenericFormEditPost(TestResultFailureData, TestResultFailureLogic,
+ WuiTestResultFailure, self.ksActionResultsUnGrouped);
+
+ def _actionViewLog(self):
+ """
+ Log viewer action.
+ """
+ from testmanager.core.testresults import TestResultLogic, TestResultFileDataEx;
+ from testmanager.core.testset import TestSetData, TestSetLogic;
+ from testmanager.webui.wuilogviewer import WuiLogViewer;
+
+ self._sTemplate = 'template-details.html'; ## @todo create new template (background color, etc)
+ idTestSet = self.getIntParam(self.ksParamLogSetId, iMin = 1);
+ idLogFile = self.getIntParam(self.ksParamLogFileId, iMin = 0, iDefault = 0);
+ cbChunk = self.getIntParam(self.ksParamLogChunkSize, iMin = 256, iMax = 16777216, iDefault = 1024*1024);
+ iChunk = self.getIntParam(self.ksParamLogChunkNo, iMin = 0,
+ iMax = config.g_kcMbMaxMainLog * 1048576 / cbChunk, iDefault = 0);
+ self._checkForUnknownParameters();
+
+ oTestSet = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ if idLogFile == 0:
+ oTestFile = TestResultFileDataEx().initFakeMainLog(oTestSet);
+ aoTimestamps = TestResultLogic(self._oDb).fetchTimestampsForLogViewer(idTestSet);
+ else:
+ oTestFile = TestSetLogic(self._oDb).getFile(idTestSet, idLogFile);
+ aoTimestamps = [];
+ if oTestFile.sMime not in [ 'text/plain',]:
+ raise WuiException('The log view does not display files of type: %s' % (oTestFile.sMime,));
+
+ oContent = WuiLogViewer(oTestSet, oTestFile, cbChunk, iChunk, aoTimestamps,
+ oDisp = self, fnDPrint = self._oSrvGlue.dprint);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
+ def _actionGetFile(self):
+ """
+ Get file action.
+ """
+ from testmanager.core.testset import TestSetData, TestSetLogic;
+ from testmanager.core.testresults import TestResultFileDataEx;
+
+ idTestSet = self.getIntParam(self.ksParamGetFileSetId, iMin = 1);
+ idFile = self.getIntParam(self.ksParamGetFileId, iMin = 0, iDefault = 0);
+ fDownloadIt = self.getBoolParam(self.ksParamGetFileDownloadIt, fDefault = True);
+ self._checkForUnknownParameters();
+
+ #
+ # Get the file info and open it.
+ #
+ oTestSet = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ if idFile == 0:
+ oTestFile = TestResultFileDataEx().initFakeMainLog(oTestSet);
+ else:
+ oTestFile = TestSetLogic(self._oDb).getFile(idTestSet, idFile);
+
+ (oFile, oSizeOrError, _) = oTestSet.openFile(oTestFile.sFile, 'rb');
+ if oFile is None:
+ raise Exception(oSizeOrError);
+
+ #
+ # Send the file.
+ #
+ self._oSrvGlue.setHeaderField('Content-Type', oTestFile.getMimeWithEncoding());
+ if fDownloadIt:
+ self._oSrvGlue.setHeaderField('Content-Disposition', 'attachment; filename="TestSet-%d-%s"'
+ % (idTestSet, oTestFile.sFile,));
+ while True:
+ abChunk = oFile.read(262144);
+ if not abChunk:
+ break;
+ self._oSrvGlue.writeRaw(abChunk);
+ return self.ksDispatchRcAllDone;
+
+ def _actionGenericReport(self, oModelType, oFilterType, oReportType):
+ """
+ Generic report action.
+ oReportType is a child of WuiReportContentBase.
+ oFilterType is a child of ModelFilterBase.
+ oModelType is a child of ReportModelBase.
+ """
+ from testmanager.core.report import ReportModelBase;
+
+ tsEffective = self.getEffectiveDateParam();
+ cPeriods = self.getIntParam(self.ksParamReportPeriods, iMin = 2, iMax = 99, iDefault = 7);
+ cHoursPerPeriod = self.getIntParam(self.ksParamReportPeriodInHours, iMin = 1, iMax = 168, iDefault = 24);
+ sSubject = self.getStringParam(self.ksParamReportSubject, ReportModelBase.kasSubjects,
+ ReportModelBase.ksSubEverything);
+ if sSubject == ReportModelBase.ksSubEverything:
+ aidSubjects = self.getListOfIntParams(self.ksParamReportSubjectIds, aiDefaults = []);
+ else:
+ aidSubjects = self.getListOfIntParams(self.ksParamReportSubjectIds, iMin = 1);
+ if aidSubjects is None:
+ raise WuiException('Missing parameter %s' % (self.ksParamReportSubjectIds,));
+
+ aiSortColumnsDup = self.getListOfIntParams(self.ksParamSortColumns,
+ iMin = -getattr(oReportType, 'kcMaxSortColumns', cPeriods) + 1,
+ iMax = getattr(oReportType, 'kcMaxSortColumns', cPeriods), aiDefaults = []);
+ aiSortColumns = [];
+ for iSortColumn in aiSortColumnsDup:
+ if iSortColumn not in aiSortColumns:
+ aiSortColumns.append(iSortColumn);
+
+ oFilter = oFilterType().initFromParams(self);
+ self._checkForUnknownParameters();
+
+ dParams = \
+ {
+ self.ksParamEffectiveDate: tsEffective,
+ self.ksParamReportPeriods: cPeriods,
+ self.ksParamReportPeriodInHours: cHoursPerPeriod,
+ self.ksParamReportSubject: sSubject,
+ self.ksParamReportSubjectIds: aidSubjects,
+ };
+ ## @todo oFilter.
+
+ oModel = oModelType(self._oDb, tsEffective, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter);
+ oContent = oReportType(oModel, dParams, fSubReport = False, aiSortColumns = aiSortColumns,
+ fnDPrint = self._oSrvGlue.dprint, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ sNavi = self._generateReportNavigation(tsEffective, cHoursPerPeriod, cPeriods);
+ self._sPageBody = sNavi + self._sPageBody;
+
+ if hasattr(oFilter, 'kiBranches'):
+ oFilter.aCriteria[oFilter.kiBranches].fExpanded = True;
+ self._sPageFilter = self._generateResultFilter(oFilter, oModel, tsEffective, '%s hours' % (cHoursPerPeriod * cPeriods,));
+ return True;
+
+ def _actionReportSummary(self):
+ """ Action wrapper. """
+ from testmanager.core.report import ReportLazyModel, ReportFilter;
+ from testmanager.webui.wuireport import WuiReportSummary;
+ return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportSummary);
+
+ def _actionReportRate(self):
+ """ Action wrapper. """
+ from testmanager.core.report import ReportLazyModel, ReportFilter;
+ from testmanager.webui.wuireport import WuiReportSuccessRate;
+ return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportSuccessRate);
+
+ def _actionReportTestCaseFailures(self):
+ """ Action wrapper. """
+ from testmanager.core.report import ReportLazyModel, ReportFilter;
+ from testmanager.webui.wuireport import WuiReportTestCaseFailures;
+ return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportTestCaseFailures);
+
+ def _actionReportFailureReasons(self):
+ """ Action wrapper. """
+ from testmanager.core.report import ReportLazyModel, ReportFilter;
+ from testmanager.webui.wuireport import WuiReportFailureReasons;
+ return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportFailureReasons);
+
+ def _actionGraphWiz(self):
+ """
+ Graph wizard action.
+ """
+ from testmanager.core.report import ReportModelBase, ReportGraphModel;
+ from testmanager.webui.wuigraphwiz import WuiGraphWiz;
+ self._sTemplate = 'template-graphwiz.html';
+
+ tsEffective = self.getEffectiveDateParam();
+ cPeriods = self.getIntParam(self.ksParamReportPeriods, iMin = 1, iMax = 1, iDefault = 1); # Not needed yet.
+ sTmp = self.getStringParam(self.ksParamReportPeriodInHours, sDefault = '3 weeks');
+ (cHoursPerPeriod, sError) = utils.parseIntervalHours(sTmp);
+ if sError is not None: raise WuiException(sError);
+ asSubjectIds = self.getListOfStrParams(self.ksParamReportSubjectIds);
+ sSubject = self.getStringParam(self.ksParamReportSubject, [ReportModelBase.ksSubEverything],
+ ReportModelBase.ksSubEverything); # dummy
+ aidTestBoxes = self.getListOfIntParams(self.ksParamGraphWizTestBoxIds, iMin = 1, aiDefaults = []);
+ aidBuildCats = self.getListOfIntParams(self.ksParamGraphWizBuildCatIds, iMin = 1, aiDefaults = []);
+ aidTestCases = self.getListOfIntParams(self.ksParamGraphWizTestCaseIds, iMin = 1, aiDefaults = []);
+ fSepTestVars = self.getBoolParam(self.ksParamGraphWizSepTestVars, fDefault = False);
+
+ enmGraphImpl = self.getStringParam(self.ksParamGraphWizImpl, asValidValues = self.kasGraphWizImplValid,
+ sDefault = self.ksGraphWizImpl_Default);
+ cx = self.getIntParam(self.ksParamGraphWizWidth, iMin = 128, iMax = 8192, iDefault = 1280);
+ cy = self.getIntParam(self.ksParamGraphWizHeight, iMin = 128, iMax = 8192, iDefault = int(cx * 5 / 16) );
+ cDotsPerInch = self.getIntParam(self.ksParamGraphWizDpi, iMin = 64, iMax = 512, iDefault = 96);
+ cPtFont = self.getIntParam(self.ksParamGraphWizFontSize, iMin = 6, iMax = 32, iDefault = 8);
+ fErrorBarY = self.getBoolParam(self.ksParamGraphWizErrorBarY, fDefault = False);
+ cMaxErrorBarY = self.getIntParam(self.ksParamGraphWizMaxErrorBarY, iMin = 8, iMax = 9999999, iDefault = 18);
+ cMaxPerGraph = self.getIntParam(self.ksParamGraphWizMaxPerGraph, iMin = 1, iMax = 24, iDefault = 8);
+ fXkcdStyle = self.getBoolParam(self.ksParamGraphWizXkcdStyle, fDefault = False);
+ fTabular = self.getBoolParam(self.ksParamGraphWizTabular, fDefault = False);
+ idSrcTestSet = self.getIntParam(self.ksParamGraphWizSrcTestSetId, iDefault = None);
+ self._checkForUnknownParameters();
+
+ dParams = \
+ {
+ self.ksParamEffectiveDate: tsEffective,
+ self.ksParamReportPeriods: cPeriods,
+ self.ksParamReportPeriodInHours: cHoursPerPeriod,
+ self.ksParamReportSubject: sSubject,
+ self.ksParamReportSubjectIds: asSubjectIds,
+ self.ksParamGraphWizTestBoxIds: aidTestBoxes,
+ self.ksParamGraphWizBuildCatIds: aidBuildCats,
+ self.ksParamGraphWizTestCaseIds: aidTestCases,
+ self.ksParamGraphWizSepTestVars: fSepTestVars,
+
+ self.ksParamGraphWizImpl: enmGraphImpl,
+ self.ksParamGraphWizWidth: cx,
+ self.ksParamGraphWizHeight: cy,
+ self.ksParamGraphWizDpi: cDotsPerInch,
+ self.ksParamGraphWizFontSize: cPtFont,
+ self.ksParamGraphWizErrorBarY: fErrorBarY,
+ self.ksParamGraphWizMaxErrorBarY: cMaxErrorBarY,
+ self.ksParamGraphWizMaxPerGraph: cMaxPerGraph,
+ self.ksParamGraphWizXkcdStyle: fXkcdStyle,
+ self.ksParamGraphWizTabular: fTabular,
+ self.ksParamGraphWizSrcTestSetId: idSrcTestSet,
+ };
+
+ oModel = ReportGraphModel(self._oDb, tsEffective, cPeriods, cHoursPerPeriod, sSubject, asSubjectIds,
+ aidTestBoxes, aidBuildCats, aidTestCases, fSepTestVars);
+ oContent = WuiGraphWiz(oModel, dParams, fSubReport = False, fnDPrint = self._oSrvGlue.dprint, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
+ def _actionVcsHistoryTooltip(self):
+ """
+ Version control system history.
+ """
+ from testmanager.webui.wuivcshistory import WuiVcsHistoryTooltip;
+ from testmanager.core.vcsrevisions import VcsRevisionLogic;
+
+ self._sTemplate = 'template-tooltip.html';
+ iRevision = self.getIntParam(self.ksParamVcsHistoryRevision, iMin = 0, iMax = 999999999);
+ sRepository = self.getStringParam(self.ksParamVcsHistoryRepository);
+ cEntries = self.getIntParam(self.ksParamVcsHistoryEntries, iMin = 1, iMax = 1024, iDefault = 8);
+ self._checkForUnknownParameters();
+
+ aoEntries = VcsRevisionLogic(self._oDb).fetchTimeline(sRepository, iRevision, cEntries);
+ oContent = WuiVcsHistoryTooltip(aoEntries, sRepository, iRevision, cEntries,
+ fnDPrint = self._oSrvGlue.dprint, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuireport.py b/src/VBox/ValidationKit/testmanager/webui/wuireport.py
new file mode 100755
index 00000000..f1625176
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuireport.py
@@ -0,0 +1,817 @@
+# -*- coding: utf-8 -*-
+# $Id: wuireport.py $
+
+"""
+Test Manager WUI - Reports.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from common import webutils;
+from testmanager.webui.wuicontentbase import WuiContentBase, WuiTmLink, WuiSvnLinkWithTooltip;
+from testmanager.webui.wuihlpgraph import WuiHlpGraphDataTable, WuiHlpBarGraph;
+from testmanager.webui.wuitestresult import WuiTestSetLink, WuiTestResultsForTestCaseLink, WuiTestResultsForTestBoxLink;
+from testmanager.webui.wuiadmintestcase import WuiTestCaseDetailsLink;
+from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLinkShort;
+from testmanager.core.report import ReportModelBase, ReportFilter;
+from testmanager.core.testresults import TestResultFilter;
+
+
+class WuiReportSummaryLink(WuiTmLink):
+ """ Generic report summary link. """
+
+ def __init__(self, sSubject, aIdSubjects, sName = WuiContentBase.ksShortReportLink,
+ tsNow = None, cPeriods = None, cHoursPerPeriod = None, fBracketed = False, dExtraParams = None):
+ from testmanager.webui.wuimain import WuiMain;
+ dParams = {
+ WuiMain.ksParamAction: WuiMain.ksActionReportSummary,
+ WuiMain.ksParamReportSubject: sSubject,
+ WuiMain.ksParamReportSubjectIds: aIdSubjects,
+ };
+ if dExtraParams is not None:
+ dParams.update(dExtraParams);
+ if tsNow is not None:
+ dParams[WuiMain.ksParamEffectiveDate] = tsNow;
+ if cPeriods is not None:
+ dParams[WuiMain.ksParamReportPeriods] = cPeriods;
+ if cPeriods is not None:
+ dParams[WuiMain.ksParamReportPeriodInHours] = cHoursPerPeriod;
+ WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams, fBracketed = fBracketed);
+
+
+class WuiReportBase(WuiContentBase):
+ """
+ Base class for the reports.
+ """
+
+ def __init__(self, oModel, dParams, fSubReport = False, aiSortColumns = None, fnDPrint = None, oDisp = None):
+ WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
+ self._oModel = oModel;
+ self._dParams = dParams;
+ self._fSubReport = fSubReport;
+ self._sTitle = None;
+ self._aiSortColumns = aiSortColumns;
+
+ # Additional URL parameters for reports:
+ from testmanager.webui.wuimain import WuiMain;
+ self._dExtraParams = ReportFilter().strainParameters({} if oDisp is None else oDisp.getParameters(),
+ (WuiMain.ksParamReportPeriods,
+ WuiMain.ksParamReportPeriodInHours,
+ WuiMain.ksParamEffectiveDate,));
+ # Additional URL parameters for test results:
+ self._dExtraTestResultsParams = TestResultFilter().strainParameters(oDisp.getParameters(),
+ (WuiMain.ksParamEffectiveDate,));
+ self._dExtraTestResultsParams[WuiMain.ksParamEffectivePeriod] = self.getPeriodForTestResults();
+
+
+ def generateNavigator(self, sWhere):
+ """
+ Generates the navigator (manipulate _dParams).
+ Returns HTML.
+ """
+ assert sWhere in ('top', 'bottom',);
+
+ return '';
+
+ def generateReportBody(self):
+ """
+ This is overridden by the child class to generate the report.
+ Returns HTML.
+ """
+ return '<h3>Must override generateReportBody!</h3>';
+
+ def show(self):
+ """
+ Generate the report.
+ Returns (sTitle, HTML).
+ """
+
+ sTitle = self._sTitle if self._sTitle is not None else type(self).__name__;
+ sReport = self.generateReportBody();
+ if not self._fSubReport:
+ sReport = self.generateNavigator('top') + sReport + self.generateNavigator('bottom');
+ sTitle = self._oModel.sSubject + ' - ' + sTitle; ## @todo add subject to title in a proper way!
+
+ sReport += '\n\n<!-- HEYYOU: sSubject=%s aidSubjects=%s -->\n\n' % (self._oModel.sSubject, self._oModel.aidSubjects);
+ return (sTitle, sReport);
+
+ #
+ # Utility methods
+ #
+
+ def getPeriodForTestResults(self):
+ """
+ Takes the report period length and count and translates it into a
+ reasonable test result period (value).
+ """
+ from testmanager.webui.wuimain import WuiMain;
+ cHours = self._oModel.cPeriods * self._oModel.cHoursPerPeriod;
+ if cHours > 7*24:
+ cHours = cHours // 2;
+ for sPeriodValue, _, cPeriodHours in WuiMain.kaoResultPeriods:
+ sPeriod = sPeriodValue;
+ if cPeriodHours >= cHours:
+ return sPeriod;
+ return sPeriod;
+
+ @staticmethod
+ def fmtPct(cHits, cTotal):
+ """
+ Formats a percent number.
+ Returns a string.
+ """
+ uPct = cHits * 100 // cTotal;
+ if uPct >= 10 and (uPct > 103 or uPct <= 95):
+ return '%s%%' % (uPct,);
+ return '%.1f%%' % (cHits * 100.0 / cTotal,);
+
+ @staticmethod
+ def fmtPctWithHits(cHits, cTotal):
+ """
+ Formats a percent number with total in parentheses.
+ Returns a string.
+ """
+ return '%s (%s)' % (WuiReportBase.fmtPct(cHits, cTotal), cHits);
+
+ @staticmethod
+ def fmtPctWithHitsAndTotal(cHits, cTotal):
+ """
+ Formats a percent number with total in parentheses.
+ Returns a string.
+ """
+ return '%s (%s/%s)' % (WuiReportBase.fmtPct(cHits, cTotal), cHits, cTotal);
+
+
+
+class WuiReportSuccessRate(WuiReportBase):
+ """
+ Generates a report displaying the success rate over time.
+ """
+
+ def generateReportBody(self):
+ self._sTitle = 'Success rate';
+ fTailoredForGoogleCharts = True;
+
+ #
+ # Get the data and check if we have anything in the 'skipped' category.
+ #
+ adPeriods = self._oModel.getSuccessRates();
+
+ cTotalSkipped = 0;
+ for dStatuses in adPeriods:
+ cTotalSkipped += dStatuses[ReportModelBase.ksTestStatus_Skipped];
+
+ #
+ # Output some general stats before the graphs.
+ #
+ cTotalNow = adPeriods[0][ReportModelBase.ksTestStatus_Success];
+ cTotalNow += adPeriods[0][ReportModelBase.ksTestStatus_Skipped];
+ cSuccessNow = cTotalNow;
+ cTotalNow += adPeriods[0][ReportModelBase.ksTestStatus_Failure];
+
+ sReport = '<p>Current success rate: ';
+ if cTotalNow > 0:
+ cSkippedNow = adPeriods[0][ReportModelBase.ksTestStatus_Skipped];
+ if cSkippedNow > 0:
+ sReport += '%s (thereof %s skipped)</p>\n' \
+ % (self.fmtPct(cSuccessNow, cTotalNow), self.fmtPct(cSkippedNow, cTotalNow),);
+ else:
+ sReport += '%s (none skipped)</p>\n' % (self.fmtPct(cSuccessNow, cTotalNow),);
+ else:
+ sReport += 'N/A</p>\n'
+
+ #
+ # Create the data table.
+ #
+ if fTailoredForGoogleCharts:
+ if cTotalSkipped > 0:
+ oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Skipped', 'Failed' ]);
+ else:
+ oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Failed' ]);
+ else:
+ if cTotalSkipped > 0:
+ oTable = WuiHlpGraphDataTable('When', [ 'Succeeded', 'Skipped', 'Failed' ]);
+ else:
+ oTable = WuiHlpGraphDataTable('When', [ 'Succeeded', 'Failed' ]);
+
+ for i, dStatuses in enumerate(adPeriods):
+ cSuccesses = dStatuses[ReportModelBase.ksTestStatus_Success];
+ cFailures = dStatuses[ReportModelBase.ksTestStatus_Failure];
+ cSkipped = dStatuses[ReportModelBase.ksTestStatus_Skipped];
+
+ cSuccess = cSuccesses + cSkipped;
+ cTotal = cSuccess + cFailures;
+ sPeriod = self._oModel.getPeriodDesc(i);
+ if fTailoredForGoogleCharts:
+ if cTotalSkipped > 0:
+ oTable.addRow(sPeriod,
+ [ cSuccesses * 100 // cTotal if cTotal else 0,
+ cSkipped * 100 // cTotal if cTotal else 0,
+ cFailures * 100 // cTotal if cTotal else 0, ],
+ [ self.fmtPct(cSuccesses, cTotal) if cSuccesses else None,
+ self.fmtPct(cSkipped, cTotal) if cSkipped else None,
+ self.fmtPct(cFailures, cTotal) if cFailures else None, ]);
+ else:
+ oTable.addRow(sPeriod,
+ [ cSuccesses * 100 // cTotal if cTotal else 0,
+ cFailures * 100 // cTotal if cTotal else 0, ],
+ [ self.fmtPct(cSuccesses, cTotal) if cSuccesses else None,
+ self.fmtPct(cFailures, cTotal) if cFailures else None, ]);
+ elif cTotal > 0:
+ if cTotalSkipped > 0:
+ oTable.addRow(sPeriod,
+ [ cSuccesses * 100 // cTotal,
+ cSkipped * 100 // cTotal,
+ cFailures * 100 // cTotal, ],
+ [ self.fmtPctWithHits(cSuccesses, cTotal),
+ self.fmtPctWithHits(cSkipped, cTotal),
+ self.fmtPctWithHits(cFailures, cTotal), ]);
+ else:
+ oTable.addRow(sPeriod,
+ [ cSuccesses * 100 // cTotal,
+ cFailures * 100 // cTotal, ],
+ [ self.fmtPctWithHits(cSuccesses, cTotal),
+ self.fmtPctWithHits(cFailures, cTotal), ]);
+ elif cTotalSkipped > 0:
+ oTable.addRow(sPeriod, [ 0, 0, 0 ], [ '0%', '0%', '0%' ]);
+ else:
+ oTable.addRow(sPeriod, [ 0, 0 ], [ '0%', '0%' ]);
+
+ #
+ # Render the graph.
+ #
+ oGraph = WuiHlpBarGraph('success-rate', oTable, self._oDisp);
+ oGraph.setRangeMax(100);
+ sReport += oGraph.renderGraph();
+
+ #
+ # Graph with absolute counts.
+ #
+ if fTailoredForGoogleCharts:
+ if cTotalSkipped > 0:
+ oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Skipped', 'Failed' ]);
+ else:
+ oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Failed' ]);
+ for i, dStatuses in enumerate(adPeriods):
+ cSuccesses = dStatuses[ReportModelBase.ksTestStatus_Success];
+ cFailures = dStatuses[ReportModelBase.ksTestStatus_Failure];
+ cSkipped = dStatuses[ReportModelBase.ksTestStatus_Skipped];
+
+ if cTotalSkipped > 0:
+ oTable.addRow(None, #self._oModel.getPeriodDesc(i),
+ [ cSuccesses, cSkipped, cFailures, ],
+ [ str(cSuccesses) if cSuccesses > 0 else None,
+ str(cSkipped) if cSkipped > 0 else None,
+ str(cFailures) if cFailures > 0 else None, ]);
+ else:
+ oTable.addRow(None, #self._oModel.getPeriodDesc(i),
+ [ cSuccesses, cFailures, ],
+ [ str(cSuccesses) if cSuccesses > 0 else None,
+ str(cFailures) if cFailures > 0 else None, ]);
+ oGraph = WuiHlpBarGraph('success-numbers', oTable, self._oDisp);
+ oGraph.invertYDirection();
+ sReport += oGraph.renderGraph();
+
+ return sReport;
+
+
+class WuiReportFailuresBase(WuiReportBase):
+ """
+ Common parent of WuiReportFailureReasons and WuiReportTestCaseFailures.
+ """
+
+ def _splitSeriesIntoMultipleGraphs(self, aidSorted, cMaxSeriesPerGraph = 8):
+ """
+ Splits the ID array into one or more arrays, making sure we don't
+ have too many series per graph.
+ Returns array of ID arrays.
+ """
+ if len(aidSorted) <= cMaxSeriesPerGraph + 2:
+ return [aidSorted,];
+ cGraphs = len(aidSorted) // cMaxSeriesPerGraph + (len(aidSorted) % cMaxSeriesPerGraph != 0);
+ cPerGraph = len(aidSorted) // cGraphs + (len(aidSorted) % cGraphs != 0);
+
+ aaoRet = [];
+ cLeft = len(aidSorted);
+ iSrc = 0;
+ while cLeft > 0:
+ cThis = cPerGraph;
+ if cLeft <= cPerGraph + 2:
+ cThis = cLeft;
+ elif cLeft <= cPerGraph * 2 + 4:
+ cThis = cLeft // 2;
+ aaoRet.append(aidSorted[iSrc : iSrc + cThis]);
+ iSrc += cThis;
+ cLeft -= cThis;
+ return aaoRet;
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ """
+ Worker for _formatEdgeOccurence that child classes overrides to format
+ their type of subject data in the best possible way.
+ """
+ _ = oTransient;
+ assert False;
+ return '';
+
+ def _formatEdgeOccurence(self, oTransient):
+ """
+ Helper for formatting the transients.
+ oTransient is of type ReportFailureReasonTransient or ReportTestCaseFailureTransient.
+ """
+ sHtml = u'<li>';
+ if oTransient.fEnter: sHtml += 'Since ';
+ else: sHtml += 'Until ';
+ sHtml += WuiSvnLinkWithTooltip(oTransient.iRevision, oTransient.sRepository, fBracketed = 'False').toHtml();
+ sHtml += u', %s: ' % (WuiTestSetLink(oTransient.idTestSet, self.formatTsShort(oTransient.tsDone),
+ fBracketed = False).toHtml(), )
+ sHtml += self._formatEdgeOccurenceSubject(oTransient);
+ sHtml += u'</li>\n';
+ return sHtml;
+
+ def _generateTransitionList(self, oSet):
+ """
+ Generates the enter and leave lists.
+ """
+ # Skip this if we're looking at builds.
+ if self._oModel.sSubject in [self._oModel.ksSubBuild,] and len(self._oModel.aidSubjects) in [1, 2]:
+ return u'';
+
+ sHtml = u'<h4>Movements:</h4>\n' \
+ u'<ul>\n';
+ if not oSet.aoEnterInfo and not oSet.aoLeaveInfo:
+ sHtml += u'<li>No changes</li>\n';
+ else:
+ for oTransient in oSet.aoEnterInfo:
+ sHtml += self._formatEdgeOccurence(oTransient);
+ for oTransient in oSet.aoLeaveInfo:
+ sHtml += self._formatEdgeOccurence(oTransient);
+ sHtml += u'</ul>\n';
+
+ return sHtml;
+
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ """ Formats the series name column for the HTML table. """
+ return '<th>Subject Name</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ """ Formats the series name for the HTML table. """
+ _ = oSet;
+ return '<td>%d</td>' % (idKey,);
+
+ def _formatRowValueForTable(self, oRow, oPeriod, cColsPerSeries):
+ """ Formats a row value for the HTML table. """
+ _ = oPeriod;
+ if oRow is None:
+ return u'<td colspan="%d"> </td>' % (cColsPerSeries,);
+ if cColsPerSeries == 2:
+ return u'<td align="right">%u%%</td><td align="center">%u / %u</td>' \
+ % (oRow.cHits * 100 // oRow.cTotal, oRow.cHits, oRow.cTotal);
+ return u'<td align="center">%u</td>' % (oRow.cHits,);
+
+ def _formatSeriesTotalForTable(self, oSet, idKey, cColsPerSeries):
+ """ Formats the totals cell for a data series in the HTML table. """
+ dcTotalPerId = getattr(oSet, 'dcTotalPerId', None);
+ if cColsPerSeries == 2:
+ return u'<td align="right">%u%%</td><td align="center">%u/%u</td>' \
+ % (oSet.dcHitsPerId[idKey] * 100 // dcTotalPerId[idKey], oSet.dcHitsPerId[idKey], dcTotalPerId[idKey]);
+ return u'<td align="center">%u</td>' % (oSet.dcHitsPerId[idKey],);
+
+ def _generateTableForSet(self, oSet, aidSorted = None, iSortColumn = 0,
+ fWithTotals = True, cColsPerSeries = None):
+ """
+ Turns the set into a table.
+
+ Returns raw html.
+ """
+ sHtml = u'<table class="tmtbl-report-set" width="100%%">\n';
+ if cColsPerSeries is None:
+ cColsPerSeries = 2 if hasattr(oSet, 'dcTotalPerId') else 1;
+
+ # Header row.
+ sHtml += u' <tr><thead><th>#</th>';
+ sHtml += self._formatSeriesNameColumnHeadersForTable();
+ for iPeriod, oPeriod in enumerate(reversed(oSet.aoPeriods)):
+ sHtml += u'<th colspan="%d"><a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">%s</a>%s</th>' \
+ % ( cColsPerSeries, self._oDisp.ksParamSortColumns, iPeriod, webutils.escapeElem(oPeriod.sDesc),
+ '&#x25bc;' if iPeriod == iSortColumn else '');
+ if fWithTotals:
+ sHtml += u'<th colspan="%d"><a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">Total</a>%s</th>' \
+ % ( cColsPerSeries, self._oDisp.ksParamSortColumns, len(oSet.aoPeriods),
+ '&#x25bc;' if iSortColumn == len(oSet.aoPeriods) else '');
+ sHtml += u'</thead></td>\n';
+
+ # Each data series.
+ if aidSorted is None:
+ aidSorted = oSet.dSubjects.keys();
+ sHtml += u' <tbody>\n';
+ for iRow, idKey in enumerate(aidSorted):
+ sHtml += u' <tr class="%s">' % ('tmodd' if iRow & 1 else 'tmeven',);
+ sHtml += u'<td align="left">#%u</td>' % (iRow + 1,);
+ sHtml += self._formatSeriesNameForTable(oSet, idKey);
+ for oPeriod in reversed(oSet.aoPeriods):
+ oRow = oPeriod.dRowsById.get(idKey, None);
+ sHtml += self._formatRowValueForTable(oRow, oPeriod, cColsPerSeries);
+ if fWithTotals:
+ sHtml += self._formatSeriesTotalForTable(oSet, idKey, cColsPerSeries);
+ sHtml += u' </tr>\n';
+ sHtml += u' </tbody>\n';
+ sHtml += u'</table>\n';
+ return sHtml;
+
+
+class WuiReportFailuresWithTotalBase(WuiReportFailuresBase):
+ """
+ For ReportPeriodSetWithTotalBase.
+ """
+
+ def _formatSeriedNameForGraph(self, oSubject):
+ """
+ Format the subject name for the graph.
+ """
+ return str(oSubject);
+
+ def _getSortedIds(self, oSet):
+ """
+ Get default sorted subject IDs and which column.
+ """
+
+ # Figure the sorting column.
+ if self._aiSortColumns is not None \
+ and self._aiSortColumns \
+ and abs(self._aiSortColumns[0]) <= len(oSet.aoPeriods):
+ iSortColumn = abs(self._aiSortColumns[0]);
+ fByTotal = iSortColumn >= len(oSet.aoPeriods); # pylint: disable=unused-variable
+ elif oSet.cMaxTotal < 10:
+ iSortColumn = len(oSet.aoPeriods);
+ else:
+ iSortColumn = 0;
+
+ if iSortColumn >= len(oSet.aoPeriods):
+ # Sort the total.
+ aidSortedRaw = sorted(oSet.dSubjects,
+ key = lambda idKey: oSet.dcHitsPerId[idKey] * 10000 // oSet.dcTotalPerId[idKey],
+ reverse = True);
+ else:
+ # Sort by NOW column.
+ dTmp = {};
+ for idKey in oSet.dSubjects:
+ oRow = oSet.aoPeriods[-1 - iSortColumn].dRowsById.get(idKey, None);
+ if oRow is None: dTmp[idKey] = 0;
+ else: dTmp[idKey] = oRow.cHits * 10000 // max(1, oRow.cTotal);
+ aidSortedRaw = sorted(dTmp, key = lambda idKey: dTmp[idKey], reverse = True);
+ return (aidSortedRaw, iSortColumn);
+
+ def _generateGraph(self, oSet, sIdBase, aidSortedRaw):
+ """
+ Generates graph.
+ """
+ sHtml = u'';
+ fGenerateGraph = len(aidSortedRaw) <= 6 and len(aidSortedRaw) > 0; ## Make this configurable.
+ if fGenerateGraph:
+ # Figure the graph width for all of them.
+ uPctMax = max(oSet.uMaxPct, oSet.cMaxHits * 100 // oSet.cMaxTotal);
+ uPctMax = max(uPctMax + 2, 10);
+
+ for _, aidSorted in enumerate(self._splitSeriesIntoMultipleGraphs(aidSortedRaw, 8)):
+ asNames = [];
+ for idKey in aidSorted:
+ oSubject = oSet.dSubjects[idKey];
+ asNames.append(self._formatSeriedNameForGraph(oSubject));
+
+ oTable = WuiHlpGraphDataTable('Period', asNames);
+
+ for _, oPeriod in enumerate(reversed(oSet.aoPeriods)):
+ aiValues = [];
+ asValues = [];
+
+ for idKey in aidSorted:
+ oRow = oPeriod.dRowsById.get(idKey, None);
+ if oRow is not None:
+ aiValues.append(oRow.cHits * 100 // oRow.cTotal);
+ asValues.append(self.fmtPctWithHitsAndTotal(oRow.cHits, oRow.cTotal));
+ else:
+ aiValues.append(0);
+ asValues.append('0');
+
+ oTable.addRow(oPeriod.sDesc, aiValues, asValues);
+
+ if True: # pylint: disable=using-constant-test
+ aiValues = [];
+ asValues = [];
+ for idKey in aidSorted:
+ uPct = oSet.dcHitsPerId[idKey] * 100 // oSet.dcTotalPerId[idKey];
+ aiValues.append(uPct);
+ asValues.append(self.fmtPctWithHitsAndTotal(oSet.dcHitsPerId[idKey], oSet.dcTotalPerId[idKey]));
+ oTable.addRow('Totals', aiValues, asValues);
+
+ oGraph = WuiHlpBarGraph(sIdBase, oTable, self._oDisp);
+ oGraph.setRangeMax(uPctMax);
+ sHtml += '<br>\n';
+ sHtml += oGraph.renderGraph();
+ return sHtml;
+
+
+
+class WuiReportFailureReasons(WuiReportFailuresBase):
+ """
+ Generates a report displaying the failure reasons over time.
+ """
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ return u'%s / %s' % ( webutils.escapeElem(oTransient.oReason.oCategory.sShort),
+ webutils.escapeElem(oTransient.oReason.sShort),);
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ return '<th>Failure Reason</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ oReason = oSet.dSubjects[idKey];
+ sHtml = u'<td>';
+ sHtml += u'%s / %s' % ( webutils.escapeElem(oReason.oCategory.sShort), webutils.escapeElem(oReason.sShort),);
+ sHtml += u'</td>';
+ return sHtml;
+
+
+ def generateReportBody(self):
+ self._sTitle = 'Failure reasons';
+
+ #
+ # Get the data and sort the data series in descending order of badness.
+ #
+ oSet = self._oModel.getFailureReasons();
+ aidSortedRaw = sorted(oSet.dSubjects, key = lambda idReason: oSet.dcHitsPerId[idReason], reverse = True);
+
+ #
+ # Generate table and transition list. These are the most useful ones with the current graph machinery.
+ #
+ sHtml = self._generateTableForSet(oSet, aidSortedRaw, len(oSet.aoPeriods));
+ sHtml += self._generateTransitionList(oSet);
+
+ #
+ # Check if most of the stuff is without any assign reason, if so, skip
+ # that part of the graph so it doesn't offset the interesting bits.
+ #
+ fIncludeWithoutReason = True;
+ for oPeriod in reversed(oSet.aoPeriods):
+ if oPeriod.cWithoutReason > oSet.cMaxHits * 4:
+ fIncludeWithoutReason = False;
+ sHtml += '<p>Warning: Many failures without assigned reason!</p>\n';
+ break;
+
+ #
+ # Generate the graph.
+ #
+ fGenerateGraph = len(aidSortedRaw) <= 9 and len(aidSortedRaw) > 0; ## Make this configurable.
+ if fGenerateGraph:
+ aidSorted = aidSortedRaw;
+
+ asNames = [];
+ for idReason in aidSorted:
+ oReason = oSet.dSubjects[idReason];
+ asNames.append('%s / %s' % (oReason.oCategory.sShort, oReason.sShort,) )
+ if fIncludeWithoutReason:
+ asNames.append('No reason');
+
+ oTable = WuiHlpGraphDataTable('Period', asNames);
+
+ cMax = oSet.cMaxHits;
+ for _, oPeriod in enumerate(reversed(oSet.aoPeriods)):
+ aiValues = [];
+
+ for idReason in aidSorted:
+ oRow = oPeriod.dRowsById.get(idReason, None);
+ iValue = oRow.cHits if oRow is not None else 0;
+ aiValues.append(iValue);
+
+ if fIncludeWithoutReason:
+ aiValues.append(oPeriod.cWithoutReason);
+ if oPeriod.cWithoutReason > cMax:
+ cMax = oPeriod.cWithoutReason;
+
+ oTable.addRow(oPeriod.sDesc, aiValues);
+
+ oGraph = WuiHlpBarGraph('failure-reason', oTable, self._oDisp);
+ oGraph.setRangeMax(max(cMax + 1, 3));
+ sHtml += oGraph.renderGraph();
+ return sHtml;
+
+
+class WuiReportTestCaseFailures(WuiReportFailuresWithTotalBase):
+ """
+ Generates a report displaying the failure reasons over time.
+ """
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oSubject.sName),);
+ sHtml += WuiTestCaseDetailsLink(oTransient.oSubject.idTestCase, fBracketed = False).toHtml();
+ return sHtml;
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ return '<th>Test Case</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ oTestCase = oSet.dSubjects[idKey];
+ return u'<td>%s %s %s</td>' % \
+ ( WuiTestResultsForTestCaseLink(idKey, oTestCase.sName, self._dExtraTestResultsParams).toHtml(),
+ WuiTestCaseDetailsLink(oTestCase.idTestCase).toHtml(),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCase.idTestCase,
+ dExtraParams = self._dExtraParams).toHtml(),);
+
+ def _formatSeriedNameForGraph(self, oSubject):
+ return oSubject.sName;
+
+ def generateReportBody(self):
+ self._sTitle = 'Test Case Failures';
+ oSet = self._oModel.getTestCaseFailures();
+ (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet);
+
+ sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn);
+ sHtml += self._generateTransitionList(oSet);
+ sHtml += self._generateGraph(oSet, 'testcase-graph', aidSortedRaw);
+ return sHtml;
+
+
+class WuiReportTestCaseArgsFailures(WuiReportFailuresWithTotalBase):
+ """
+ Generates a report displaying the failure reasons over time.
+ """
+
+ def __init__(self, oModel, dParams, fSubReport = False, aiSortColumns = None, fnDPrint = None, oDisp = None):
+ WuiReportFailuresWithTotalBase.__init__(self, oModel, dParams, fSubReport = fSubReport,
+ aiSortColumns = aiSortColumns, fnDPrint = fnDPrint, oDisp = oDisp);
+ self.oTestCaseCrit = TestResultFilter().aCriteria[TestResultFilter.kiTestCases] # type: FilterCriterion
+
+ @staticmethod
+ def _formatName(oTestCaseArgs):
+ """ Internal helper for formatting the testcase name. """
+ if oTestCaseArgs.sSubName:
+ sName = u'%s / %s' % ( oTestCaseArgs.oTestCase.sName, oTestCaseArgs.sSubName, );
+ else:
+ sName = u'%s / #%u' % ( oTestCaseArgs.oTestCase.sName, oTestCaseArgs.idTestCaseArgs, );
+ return sName;
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ sHtml = u'%s ' % ( webutils.escapeElem(self._formatName(oTransient.oSubject)),);
+ sHtml += WuiTestCaseDetailsLink(oTransient.oSubject.idTestCase, fBracketed = False).toHtml();
+ return sHtml;
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ return '<th>Test Case / Variation</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ oTestCaseArgs = oSet.dSubjects[idKey];
+ sHtml = u'<td>';
+ dParams = dict(self._dExtraTestResultsParams);
+ dParams[self.oTestCaseCrit.sVarNm] = oTestCaseArgs.idTestCase;
+ dParams[self.oTestCaseCrit.oSub.sVarNm] = idKey;
+ sHtml += WuiTestResultsForTestCaseLink(oTestCaseArgs.idTestCase, self._formatName(oTestCaseArgs), dParams).toHtml();
+ sHtml += u' ';
+ sHtml += WuiTestCaseDetailsLink(oTestCaseArgs.idTestCase).toHtml();
+ #sHtml += u' ';
+ #sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestCaseArgs, oTestCaseArgs.idTestCaseArgs,
+ # sName = self._formatName(oTestCaseArgs), dExtraParams = self._dExtraParams).toHtml();
+ sHtml += u'</td>';
+ return sHtml;
+
+ def _formatSeriedNameForGraph(self, oSubject):
+ return self._formatName(oSubject);
+
+ def generateReportBody(self):
+ self._sTitle = 'Test Case Variation Failures';
+ oSet = self._oModel.getTestCaseVariationFailures();
+ (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet);
+
+ sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn);
+ sHtml += self._generateTransitionList(oSet);
+ sHtml += self._generateGraph(oSet, 'testcasearg-graph', aidSortedRaw);
+ return sHtml;
+
+
+
+class WuiReportTestBoxFailures(WuiReportFailuresWithTotalBase):
+ """
+ Generates a report displaying the failure reasons over time.
+ """
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oSubject.sName),);
+ sHtml += WuiTestBoxDetailsLinkShort(oTransient.oSubject).toHtml();
+ return sHtml;
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ return '<th colspan="5">Test Box</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ oTestBox = oSet.dSubjects[idKey];
+ sHtml = u'<td>';
+ sHtml += WuiTestResultsForTestBoxLink(idKey, oTestBox.sName, self._dExtraTestResultsParams).toHtml()
+ sHtml += u' ';
+ sHtml += WuiTestBoxDetailsLinkShort(oTestBox).toHtml();
+ sHtml += u' ';
+ sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oTestBox.idTestBox,
+ dExtraParams = self._dExtraParams).toHtml();
+ sHtml += u'</td>';
+ sOsAndVer = '%s %s' % (oTestBox.sOs, oTestBox.sOsVersion.strip(),);
+ if len(sOsAndVer) < 22:
+ sHtml += u'<td>%s</td>' % (webutils.escapeElem(sOsAndVer),);
+ else: # wonder if td.title works..
+ sHtml += u'<td title="%s" width="1%%" style="white-space:nowrap;">%s...</td>' \
+ % (webutils.escapeAttr(sOsAndVer), webutils.escapeElem(sOsAndVer[:20]));
+ sHtml += u'<td>%s</td>' % (webutils.escapeElem(oTestBox.getArchBitString()),);
+ sHtml += u'<td>%s</td>' % (webutils.escapeElem(oTestBox.getPrettyCpuVendor()),);
+ sHtml += u'<td>%s' % (oTestBox.getPrettyCpuVersion(),);
+ if oTestBox.fCpuNestedPaging: sHtml += u', np';
+ elif oTestBox.fCpuHwVirt: sHtml += u', hw';
+ else: sHtml += u', raw';
+ if oTestBox.fCpu64BitGuest: sHtml += u', 64';
+ sHtml += u'</td>';
+ return sHtml;
+
+ def _formatSeriedNameForGraph(self, oSubject):
+ return oSubject.sName;
+
+ def generateReportBody(self):
+ self._sTitle = 'Test Box Failures';
+ oSet = self._oModel.getTestBoxFailures();
+ (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet);
+
+ sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn);
+ sHtml += self._generateTransitionList(oSet);
+ sHtml += self._generateGraph(oSet, 'testbox-graph', aidSortedRaw);
+ return sHtml;
+
+
+class WuiReportSummary(WuiReportBase):
+ """
+ Summary report.
+ """
+
+ def generateReportBody(self):
+ self._sTitle = 'Summary';
+ sHtml = '<p>This will display several reports and listings useful to get an overview of %s (id=%s).</p>' \
+ % (self._oModel.sSubject, self._oModel.aidSubjects,);
+
+ aoReports = [];
+
+ aoReports.append(WuiReportSuccessRate( self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+ aoReports.append(WuiReportTestCaseFailures(self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+ if self._oModel.sSubject == ReportModelBase.ksSubTestCase:
+ aoReports.append(WuiReportTestCaseArgsFailures(self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+ aoReports.append(WuiReportTestBoxFailures( self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+ aoReports.append(WuiReportFailureReasons( self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+
+ for oReport in aoReports:
+ (sTitle, sContent) = oReport.show();
+ sHtml += '<br>'; # drop this layout hack
+ sHtml += '<div>';
+ sHtml += '<h3>%s</h3>\n' % (webutils.escapeElem(sTitle),);
+ sHtml += sContent;
+ sHtml += '</div>';
+
+ return sHtml;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py b/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py
new file mode 100755
index 00000000..1edb03d5
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py
@@ -0,0 +1,965 @@
+# -*- coding: utf-8 -*-
+# $Id: wuitestresult.py $
+
+"""
+Test Manager WUI - Test Results.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Python imports.
+import datetime;
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiContentBase, WuiListContentBase, WuiHtmlBase, WuiTmLink, WuiLinkBase, \
+ WuiSvnLink, WuiSvnLinkWithTooltip, WuiBuildLogLink, WuiRawHtml, \
+ WuiHtmlKeeper;
+from testmanager.webui.wuimain import WuiMain;
+from testmanager.webui.wuihlpform import WuiHlpForm;
+from testmanager.webui.wuiadminfailurereason import WuiFailureReasonAddLink, WuiFailureReasonDetailsLink;
+from testmanager.webui.wuitestresultfailure import WuiTestResultFailureDetailsLink;
+from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+from testmanager.core.report import ReportGraphModel, ReportModelBase;
+from testmanager.core.testbox import TestBoxData;
+from testmanager.core.testcase import TestCaseData;
+from testmanager.core.testset import TestSetData;
+from testmanager.core.testgroup import TestGroupData;
+from testmanager.core.testresultfailures import TestResultFailureData;
+from testmanager.core.build import BuildData;
+from testmanager.core import db;
+from testmanager import config;
+from common import webutils, utils;
+
+
+class WuiTestSetLink(WuiTmLink):
+ """ Test set link. """
+
+ def __init__(self, idTestSet, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False):
+ WuiTmLink.__init__(self, sName, WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: idTestSet, }, fBracketed = fBracketed);
+ self.idTestSet = idTestSet;
+
+class WuiTestResultsForSomethingLink(WuiTmLink):
+ """ Test results link for a grouping. """
+
+ def __init__(self, sGroupedBy, idGroupMember, sName = WuiContentBase.ksShortTestResultsLink,
+ dExtraParams = None, fBracketed = False):
+ dParams = dict(dExtraParams) if dExtraParams else {};
+ dParams[WuiMain.ksParamAction] = sGroupedBy;
+ dParams[WuiMain.ksParamGroupMemberId] = idGroupMember;
+ WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams, fBracketed = fBracketed);
+
+
+class WuiTestResultsForTestBoxLink(WuiTestResultsForSomethingLink):
+ """ Test results link for a given testbox. """
+
+ def __init__(self, idTestBox, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False):
+ WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByTestBox, idTestBox,
+ sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed);
+
+
+class WuiTestResultsForTestCaseLink(WuiTestResultsForSomethingLink):
+ """ Test results link for a given testcase. """
+
+ def __init__(self, idTestCase, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False):
+ WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByTestCase, idTestCase,
+ sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed);
+
+
+class WuiTestResultsForBuildRevLink(WuiTestResultsForSomethingLink):
+ """ Test results link for a given build revision. """
+
+ def __init__(self, iRevision, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False):
+ WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByBuildRev, iRevision,
+ sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed);
+
+
+class WuiTestResult(WuiContentBase):
+ """Display test case result"""
+
+ def __init__(self, fnDPrint = None, oDisp = None):
+ WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
+
+ # Cyclic import hacks.
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ self.oWuiAdmin = WuiAdmin;
+
+ def _toHtml(self, oObject):
+ """Translate some object to HTML."""
+ if isinstance(oObject, WuiHtmlBase):
+ return oObject.toHtml();
+ if db.isDbTimestamp(oObject):
+ return webutils.escapeElem(self.formatTsShort(oObject));
+ if db.isDbInterval(oObject):
+ return webutils.escapeElem(self.formatIntervalShort(oObject));
+ if utils.isString(oObject):
+ return webutils.escapeElem(oObject);
+ return webutils.escapeElem(str(oObject));
+
+ def _htmlTable(self, aoTableContent):
+ """Generate HTML code for table"""
+ sHtml = u' <table class="tmtbl-testresult-details" width="100%%">\n';
+
+ for aoSubRows in aoTableContent:
+ if not aoSubRows:
+ continue; # Can happen if there is no testsuit.
+ oCaption = aoSubRows[0];
+ sHtml += u' \n' \
+ u' <tr class="tmtbl-result-details-caption">\n' \
+ u' <td colspan="2">%s</td>\n' \
+ u' </tr>\n' \
+ % (self._toHtml(oCaption),);
+
+ iRow = 0;
+ for aoRow in aoSubRows[1:]:
+ iRow += 1;
+ sHtml += u' <tr class="%s">\n' % ('tmodd' if iRow & 1 else 'tmeven',);
+ if len(aoRow) == 1:
+ sHtml += u' <td class="tmtbl-result-details-subcaption" colspan="2">%s</td>\n' \
+ % (self._toHtml(aoRow[0]),);
+ else:
+ sHtml += u' <th scope="row">%s</th>\n' % (webutils.escapeElem(aoRow[0]),);
+ if len(aoRow) > 2:
+ sHtml += u' <td>%s</td>\n' % (aoRow[2](aoRow[1]),);
+ else:
+ sHtml += u' <td>%s</td>\n' % (self._toHtml(aoRow[1]),);
+ sHtml += u' </tr>\n';
+
+ sHtml += u' </table>\n';
+
+ return sHtml
+
+ def _highlightStatus(self, sStatus):
+ """Return sStatus string surrounded by HTML highlight code """
+ sTmp = '<font color=%s><b>%s</b></font>' \
+ % ('red' if sStatus == 'failure' else 'green', webutils.escapeElem(sStatus.upper()))
+ return sTmp
+
+ def _anchorAndAppendBinaries(self, sBinaries, aoRows):
+ """ Formats each binary (if any) into a row with a download link. """
+ if sBinaries is not None:
+ for sBinary in sBinaries.split(','):
+ if not webutils.hasSchema(sBinary):
+ sBinary = config.g_ksBuildBinUrlPrefix + sBinary;
+ aoRows.append([WuiLinkBase(webutils.getFilename(sBinary), sBinary, fBracketed = False),]);
+ return aoRows;
+
+
+ def _formatEventTimestampHtml(self, tsEvent, tsLog, idEvent, oTestSet):
+ """ Formats an event timestamp with a main log link. """
+ tsEvent = db.dbTimestampToZuluDatetime(tsEvent);
+ #sFormattedTimestamp = u'%04u\u2011%02u\u2011%02u\u00a0%02u:%02u:%02uZ' \
+ # % ( tsEvent.year, tsEvent.month, tsEvent.day,
+ # tsEvent.hour, tsEvent.minute, tsEvent.second,);
+ sFormattedTimestamp = u'%02u:%02u:%02uZ' \
+ % ( tsEvent.hour, tsEvent.minute, tsEvent.second,);
+ sTitle = u'#%u - %04u\u2011%02u\u2011%02u\u00a0%02u:%02u:%02u.%06uZ' \
+ % ( idEvent, tsEvent.year, tsEvent.month, tsEvent.day,
+ tsEvent.hour, tsEvent.minute, tsEvent.second, tsEvent.microsecond, );
+ tsLog = db.dbTimestampToZuluDatetime(tsLog);
+ sFragment = u'%02u_%02u_%02u_%06u' % ( tsLog.hour, tsLog.minute, tsLog.second, tsLog.microsecond);
+ return WuiTmLink(sFormattedTimestamp, '',
+ { WuiMain.ksParamAction: WuiMain.ksActionViewLog,
+ WuiMain.ksParamLogSetId: oTestSet.idTestSet, },
+ sFragmentId = sFragment, sTitle = sTitle, fBracketed = False, ).toHtml();
+
+ def _recursivelyGenerateEvents(self, oTestResult, sParentName, sLineage, iRow,
+ iFailure, oTestSet, iDepth): # pylint: disable=too-many-locals
+ """
+ Recursively generate event table rows for the result set.
+
+ oTestResult is an object of the type TestResultDataEx.
+ """
+ # Hack: Replace empty outer test result name with (pretty) command line.
+ if iRow == 1:
+ sName = '';
+ sDisplayName = sParentName;
+ else:
+ sName = oTestResult.sName if sParentName == '' else '%s, %s' % (sParentName, oTestResult.sName,);
+ sDisplayName = webutils.escapeElem(sName);
+
+ # Format error count.
+ sErrCnt = '';
+ if oTestResult.cErrors > 0:
+ sErrCnt = ' (1 error)' if oTestResult.cErrors == 1 else ' (%d errors)' % oTestResult.cErrors;
+
+ # Format bits for adding or editing the failure reason. Level 0 is handled at the top of the page.
+ sChangeReason = '';
+ if oTestResult.cErrors > 0 and iDepth > 0 and self._oDisp is not None and not self._oDisp.isReadOnlyUser():
+ dTmp = {
+ self._oDisp.ksParamAction: self._oDisp.ksActionTestResultFailureAdd if oTestResult.oReason is None else
+ self._oDisp.ksActionTestResultFailureEdit,
+ TestResultFailureData.ksParam_idTestResult: oTestResult.idTestResult,
+ };
+ sChangeReason = ' <a href="?%s" class="tmtbl-edit-reason" onclick="addRedirectToAnchorHref(this)">%s</a> ' \
+ % ( webutils.encodeUrlParams(dTmp), WuiContentBase.ksShortEditLinkHtml );
+
+ # Format the include in graph checkboxes.
+ sLineage += ':%u' % (oTestResult.idStrName,);
+ sResultGraph = '<input type="checkbox" name="%s" value="%s%s" title="Include result in graph."/>' \
+ % (WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeResult, sLineage,);
+ sElapsedGraph = '';
+ if oTestResult.tsElapsed is not None:
+ sElapsedGraph = '<input type="checkbox" name="%s" value="%s%s" title="Include elapsed time in graph."/>' \
+ % ( WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeElapsed, sLineage);
+
+
+ if not oTestResult.aoChildren \
+ and len(oTestResult.aoValues) + len(oTestResult.aoMsgs) + len(oTestResult.aoFiles) == 0:
+ # Leaf - single row.
+ tsEvent = oTestResult.tsCreated;
+ if oTestResult.tsElapsed is not None:
+ tsEvent += oTestResult.tsElapsed;
+ sHtml = ' <tr class="%s tmtbl-events-leaf tmtbl-events-lvl%s tmstatusrow-%s" id="S%u">\n' \
+ ' <td id="E%u">%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td colspan="2"%s>%s%s%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, oTestResult.enmStatus, oTestResult.idTestResult,
+ oTestResult.idTestResult,
+ self._formatEventTimestampHtml(tsEvent, oTestResult.tsCreated, oTestResult.idTestResult, oTestSet),
+ sElapsedGraph,
+ webutils.escapeElem(self.formatIntervalShort(oTestResult.tsElapsed)) if oTestResult.tsElapsed is not None
+ else '',
+ sDisplayName,
+ ' id="failure-%u"' % (iFailure,) if oTestResult.isFailure() else '',
+ webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt),
+ sChangeReason if oTestResult.oReason is None else '',
+ sResultGraph );
+ iRow += 1;
+ else:
+ # Multiple rows.
+ sHtml = ' <tr class="%s tmtbl-events-first tmtbl-events-lvl%s ">\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td colspan="2">%s</td>\n' \
+ ' <td></td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ self._formatEventTimestampHtml(oTestResult.tsCreated, oTestResult.tsCreated,
+ oTestResult.idTestResult, oTestSet),
+ sDisplayName,
+ 'running' if oTestResult.tsElapsed is None else '', );
+ iRow += 1;
+
+ # Depth. Check if our error count is just reflecting the one of our children.
+ cErrorsBelow = 0;
+ for oChild in oTestResult.aoChildren:
+ (sChildHtml, iRow, iFailure) = self._recursivelyGenerateEvents(oChild, sName, sLineage,
+ iRow, iFailure, oTestSet, iDepth + 1);
+ sHtml += sChildHtml;
+ cErrorsBelow += oChild.cErrors;
+
+ # Messages.
+ for oMsg in oTestResult.aoMsgs:
+ sHtml += ' <tr class="%s tmtbl-events-message tmtbl-events-lvl%s">\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td colspan="3">%s: %s</td>\n' \
+ ' <td></td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ self._formatEventTimestampHtml(oMsg.tsCreated, oMsg.tsCreated, oMsg.idTestResultMsg, oTestSet),
+ webutils.escapeElem(oMsg.enmLevel),
+ webutils.escapeElem(oMsg.sMsg), );
+ iRow += 1;
+
+ # Values.
+ for oValue in oTestResult.aoValues:
+ sHtml += ' <tr class="%s tmtbl-events-value tmtbl-events-lvl%s">\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td class="tmtbl-events-number">%s</td>\n' \
+ ' <td class="tmtbl-events-unit">%s</td>\n' \
+ ' <td><input type="checkbox" name="%s" value="%s%s:%u" title="Include value in graph."></td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ self._formatEventTimestampHtml(oValue.tsCreated, oValue.tsCreated, oValue.idTestResultValue, oTestSet),
+ webutils.escapeElem(oValue.sName),
+ utils.formatNumber(oValue.lValue).replace(' ', '&nbsp;'),
+ webutils.escapeElem(oValue.sUnit),
+ WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeValue, sLineage, oValue.idStrName, );
+ iRow += 1;
+
+ # Files.
+ for oFile in oTestResult.aoFiles:
+ if oFile.sMime in [ 'text/plain', ]:
+ aoLinks = [
+ WuiTmLink('%s (%s)' % (oFile.sFile, oFile.sKind), '',
+ { self._oDisp.ksParamAction: self._oDisp.ksActionViewLog,
+ self._oDisp.ksParamLogSetId: oTestSet.idTestSet,
+ self._oDisp.ksParamLogFileId: oFile.idTestResultFile, },
+ sTitle = oFile.sDescription),
+ WuiTmLink('View Raw', '',
+ { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile,
+ self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet,
+ self._oDisp.ksParamGetFileId: oFile.idTestResultFile,
+ self._oDisp.ksParamGetFileDownloadIt: False, },
+ sTitle = oFile.sDescription),
+ ]
+ else:
+ aoLinks = [
+ WuiTmLink('%s (%s)' % (oFile.sFile, oFile.sKind), '',
+ { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile,
+ self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet,
+ self._oDisp.ksParamGetFileId: oFile.idTestResultFile,
+ self._oDisp.ksParamGetFileDownloadIt: False, },
+ sTitle = oFile.sDescription),
+ ]
+ aoLinks.append(WuiTmLink('Download', '',
+ { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile,
+ self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet,
+ self._oDisp.ksParamGetFileId: oFile.idTestResultFile,
+ self._oDisp.ksParamGetFileDownloadIt: True, },
+ sTitle = oFile.sDescription));
+
+ sHtml += ' <tr class="%s tmtbl-events-file tmtbl-events-lvl%s">\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ self._formatEventTimestampHtml(oFile.tsCreated, oFile.tsCreated, oFile.idTestResultFile, oTestSet),
+ '\n'.join(oLink.toHtml() for oLink in aoLinks),);
+ iRow += 1;
+
+ # Done?
+ if oTestResult.tsElapsed is not None:
+ tsEvent = oTestResult.tsCreated + oTestResult.tsElapsed;
+ sHtml += ' <tr class="%s tmtbl-events-final tmtbl-events-lvl%s tmstatusrow-%s" id="E%d">\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td colspan="2"%s>%s%s%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, oTestResult.enmStatus, oTestResult.idTestResult,
+ self._formatEventTimestampHtml(tsEvent, tsEvent, oTestResult.idTestResult, oTestSet),
+ sElapsedGraph,
+ webutils.escapeElem(self.formatIntervalShort(oTestResult.tsElapsed)),
+ sDisplayName,
+ ' id="failure-%u"' % (iFailure,) if oTestResult.isFailure() else '',
+ webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt),
+ sChangeReason if cErrorsBelow < oTestResult.cErrors and oTestResult.oReason is None else '',
+ sResultGraph);
+ iRow += 1;
+
+ # Failure reason.
+ if oTestResult.oReason is not None:
+ sReasonText = '%s / %s' % ( oTestResult.oReason.oFailureReason.oCategory.sShort,
+ oTestResult.oReason.oFailureReason.sShort, );
+ sCommentHtml = '';
+ if oTestResult.oReason.sComment and oTestResult.oReason.sComment.strip():
+ sCommentHtml = '<br>' + webutils.escapeElem(oTestResult.oReason.sComment.strip());
+ sCommentHtml = sCommentHtml.replace('\n', '<br>');
+
+ sDetailedReason = ' <a href="?%s" class="tmtbl-show-reason">%s</a>' \
+ % ( webutils.encodeUrlParams({ self._oDisp.ksParamAction:
+ self._oDisp.ksActionTestResultFailureDetails,
+ TestResultFailureData.ksParam_idTestResult:
+ oTestResult.idTestResult,}),
+ WuiContentBase.ksShortDetailsLinkHtml,);
+
+ sHtml += ' <tr class="%s tmtbl-events-reason tmtbl-events-lvl%s">\n' \
+ ' <td>%s</td>\n' \
+ ' <td colspan="2">%s</td>\n' \
+ ' <td colspan="3">%s%s%s%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ webutils.escapeElem(self.formatTsShort(oTestResult.oReason.tsEffective)),
+ oTestResult.oReason.oAuthor.sUsername,
+ webutils.escapeElem(sReasonText), sDetailedReason, sChangeReason,
+ sCommentHtml,
+ 'todo');
+ iRow += 1;
+
+ if oTestResult.isFailure():
+ iFailure += 1;
+
+ return (sHtml, iRow, iFailure);
+
+
+ def _generateMainReason(self, oTestResultTree, oTestSet):
+ """
+ Generates the form for displaying and updating the main failure reason.
+
+ oTestResultTree is an instance TestResultDataEx.
+ oTestSet is an instance of TestSetData.
+
+ """
+ _ = oTestSet;
+ sHtml = ' ';
+
+ if oTestResultTree.isFailure() or oTestResultTree.cErrors > 0:
+ sHtml += ' <h2>Failure Reason:</h2>\n';
+ oData = oTestResultTree.oReason;
+
+ # We need the failure reasons for the combobox.
+ aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Test Sheriff, you figure out why!');
+ assert aoFailureReasons;
+
+ # For now we'll use the standard form helper.
+ sFormActionUrl = '%s?%s=%s' % ( self._oDisp.ksScriptName, self._oDisp.ksParamAction,
+ WuiMain.ksActionTestResultFailureAddPost if oData is None else
+ WuiMain.ksActionTestResultFailureEditPost )
+ fReadOnly = not self._oDisp or self._oDisp.isReadOnlyUser();
+ oForm = WuiHlpForm('failure-reason', sFormActionUrl,
+ sOnSubmit = WuiHlpForm.ksOnSubmit_AddReturnToFieldWithCurrentUrl, fReadOnly = fReadOnly);
+ oForm.addTextHidden(TestResultFailureData.ksParam_idTestResult, oTestResultTree.idTestResult);
+ oForm.addTextHidden(TestResultFailureData.ksParam_idTestSet, oTestSet.idTestSet);
+ if oData is not None:
+ oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason, 'Reason',
+ aoFailureReasons,
+ sPostHtml = u' ' + WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml()
+ + (u' ' + WuiFailureReasonAddLink('New', fBracketed = False).toHtml()
+ if not fReadOnly else u''));
+ oForm.addMultilineText(TestResultFailureData.ksParam_sComment, oData.sComment, 'Comment')
+
+ oForm.addNonText(u'%s (%s), %s'
+ % ( oData.oAuthor.sUsername, oData.oAuthor.sUsername,
+ self.formatTsShort(oData.tsEffective),),
+ 'Sheriff',
+ sPostHtml = ' ' + WuiTestResultFailureDetailsLink(oData.idTestResult, "Show Details").toHtml() )
+
+ oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, oData.tsEffective);
+ oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, oData.tsExpire);
+ oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor);
+ oForm.addSubmit('Change Reason');
+ else:
+ oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, -1, 'Reason', aoFailureReasons,
+ sPostHtml = ' ' + WuiFailureReasonAddLink('New').toHtml() if not fReadOnly else '');
+ oForm.addMultilineText(TestResultFailureData.ksParam_sComment, '', 'Comment');
+ oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, '');
+ oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, '');
+ oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, '');
+ oForm.addSubmit('Add Reason');
+
+ sHtml += oForm.finalize();
+ return sHtml;
+
+
+ def showTestCaseResultDetails(self, # pylint: disable=too-many-locals,too-many-statements
+ oTestResultTree,
+ oTestSet,
+ oBuildEx,
+ oValidationKitEx,
+ oTestBox,
+ oTestGroup,
+ oTestCaseEx,
+ oTestVarEx):
+ """Show detailed result"""
+ def getTcDepsHtmlList(aoTestCaseData):
+ """Get HTML <ul> list of Test Case name items"""
+ if aoTestCaseData:
+ sTmp = '<ul>'
+ for oTestCaseData in aoTestCaseData:
+ sTmp += '<li>%s</li>' % (webutils.escapeElem(oTestCaseData.sName),);
+ sTmp += '</ul>'
+ else:
+ sTmp = 'No items'
+ return sTmp
+
+ def getGrDepsHtmlList(aoGlobalResourceData):
+ """Get HTML <ul> list of Global Resource name items"""
+ if aoGlobalResourceData:
+ sTmp = '<ul>'
+ for oGlobalResourceData in aoGlobalResourceData:
+ sTmp += '<li>%s</li>' % (webutils.escapeElem(oGlobalResourceData.sName),);
+ sTmp += '</ul>'
+ else:
+ sTmp = 'No items'
+ return sTmp
+
+
+ asHtml = []
+
+ from testmanager.webui.wuireport import WuiReportSummaryLink;
+ tsReportEffectiveDate = None;
+ if oTestSet.tsDone is not None:
+ tsReportEffectiveDate = oTestSet.tsDone + datetime.timedelta(days = 4);
+ if tsReportEffectiveDate >= self.getNowTs():
+ tsReportEffectiveDate = None;
+
+ # Test result + test set details.
+ aoResultRows = [
+ WuiHtmlKeeper([ WuiTmLink(oTestCaseEx.sName, self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idTestCase: oTestCaseEx.idTestCase,
+ self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsConfig, },
+ fBracketed = False),
+ WuiTestResultsForTestCaseLink(oTestCaseEx.idTestCase),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCaseEx.idTestCase,
+ tsNow = tsReportEffectiveDate, fBracketed = False),
+ ]),
+ ];
+ if oTestCaseEx.sDescription:
+ aoResultRows.append([oTestCaseEx.sDescription,]);
+ aoResultRows.append([ 'Status:', WuiRawHtml('<span class="tmspan-status-%s">%s</span>'
+ % (oTestResultTree.enmStatus, oTestResultTree.enmStatus,))]);
+ if oTestResultTree.cErrors > 0:
+ aoResultRows.append(( 'Errors:', oTestResultTree.cErrors ));
+ aoResultRows.append([ 'Elapsed:', oTestResultTree.tsElapsed ]);
+ cSecCfgTimeout = oTestCaseEx.cSecTimeout if oTestVarEx.cSecTimeout is None else oTestVarEx.cSecTimeout;
+ cSecEffTimeout = cSecCfgTimeout * oTestBox.pctScaleTimeout / 100;
+ aoResultRows.append([ 'Timeout:',
+ '%s (%s sec)' % (utils.formatIntervalSeconds2(cSecEffTimeout), cSecEffTimeout,) ]);
+ if cSecEffTimeout != cSecCfgTimeout:
+ aoResultRows.append([ 'Cfg Timeout:',
+ '%s (%s sec)' % (utils.formatIntervalSeconds(cSecCfgTimeout), cSecCfgTimeout,) ]);
+ aoResultRows += [
+ ( 'Started:', WuiTmLink(self.formatTsShort(oTestSet.tsCreated), WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionResultsUnGrouped,
+ WuiMain.ksParamEffectiveDate: oTestSet.tsCreated, },
+ fBracketed = False) ),
+ ];
+ if oTestSet.tsDone is not None:
+ aoResultRows += [ ( 'Done:',
+ WuiTmLink(self.formatTsShort(oTestSet.tsDone), WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionResultsUnGrouped,
+ WuiMain.ksParamEffectiveDate: oTestSet.tsDone, },
+ fBracketed = False) ) ];
+ else:
+ aoResultRows += [( 'Done:', 'Still running...')];
+ aoResultRows += [( 'Config:', oTestSet.tsConfig )];
+ if oTestVarEx.cGangMembers > 1:
+ aoResultRows.append([ 'Member No:', '#%s (of %s)' % (oTestSet.iGangMemberNo, oTestVarEx.cGangMembers) ]);
+
+ aoResultRows += [
+ ( 'Test Group:',
+ WuiHtmlKeeper([ WuiTmLink(oTestGroup.sName, self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestGroupDetails,
+ TestGroupData.ksParam_idTestGroup: oTestGroup.idTestGroup,
+ self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsConfig, },
+ fBracketed = False),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestGroup, oTestGroup.idTestGroup,
+ tsNow = tsReportEffectiveDate, fBracketed = False),
+ ]), ),
+ ];
+ if oTestVarEx.sTestBoxReqExpr is not None:
+ aoResultRows.append([ 'TestBox reqs:', oTestVarEx.sTestBoxReqExpr ]);
+ elif oTestCaseEx.sTestBoxReqExpr is not None or oTestVarEx.sTestBoxReqExpr is not None:
+ aoResultRows.append([ 'TestBox reqs:', oTestCaseEx.sTestBoxReqExpr ]);
+ if oTestVarEx.sBuildReqExpr is not None:
+ aoResultRows.append([ 'Build reqs:', oTestVarEx.sBuildReqExpr ]);
+ elif oTestCaseEx.sBuildReqExpr is not None or oTestVarEx.sBuildReqExpr is not None:
+ aoResultRows.append([ 'Build reqs:', oTestCaseEx.sBuildReqExpr ]);
+ if oTestCaseEx.sValidationKitZips is not None and oTestCaseEx.sValidationKitZips != '@VALIDATIONKIT_ZIP@':
+ aoResultRows.append([ 'Validation Kit:', oTestCaseEx.sValidationKitZips ]);
+ if oTestCaseEx.aoDepTestCases:
+ aoResultRows.append([ 'Prereq. Test Cases:', oTestCaseEx.aoDepTestCases, getTcDepsHtmlList ]);
+ if oTestCaseEx.aoDepGlobalResources:
+ aoResultRows.append([ 'Global Resources:', oTestCaseEx.aoDepGlobalResources, getGrDepsHtmlList ]);
+
+ # Builds.
+ aoBuildRows = [];
+ if oBuildEx is not None:
+ aoBuildRows += [
+ WuiHtmlKeeper([ WuiTmLink('Build', self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oBuildEx.idBuild,
+ self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsCreated, },
+ fBracketed = False),
+ WuiTestResultsForBuildRevLink(oBuildEx.iRevision),
+ WuiReportSummaryLink(ReportModelBase.ksSubBuild, oBuildEx.idBuild,
+ tsNow = tsReportEffectiveDate, fBracketed = False), ]),
+ ];
+ self._anchorAndAppendBinaries(oBuildEx.sBinaries, aoBuildRows);
+ aoBuildRows += [
+ ( 'Revision:', WuiSvnLinkWithTooltip(oBuildEx.iRevision, oBuildEx.oCat.sRepository,
+ fBracketed = False) ),
+ ( 'Product:', oBuildEx.oCat.sProduct ),
+ ( 'Branch:', oBuildEx.oCat.sBranch ),
+ ( 'Type:', oBuildEx.oCat.sType ),
+ ( 'Version:', oBuildEx.sVersion ),
+ ( 'Created:', oBuildEx.tsCreated ),
+ ];
+ if oBuildEx.uidAuthor is not None:
+ aoBuildRows += [ ( 'Author ID:', oBuildEx.uidAuthor ), ];
+ if oBuildEx.sLogUrl is not None:
+ aoBuildRows += [ ( 'Log:', WuiBuildLogLink(oBuildEx.sLogUrl, fBracketed = False) ), ];
+
+ aoValidationKitRows = [];
+ if oValidationKitEx is not None:
+ aoValidationKitRows += [
+ WuiTmLink('Validation Kit', self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oValidationKitEx.idBuild,
+ self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsCreated, },
+ fBracketed = False),
+ ];
+ self._anchorAndAppendBinaries(oValidationKitEx.sBinaries, aoValidationKitRows);
+ aoValidationKitRows += [ ( 'Revision:', WuiSvnLink(oValidationKitEx.iRevision, fBracketed = False) ) ];
+ if oValidationKitEx.oCat.sProduct != 'VBox TestSuite':
+ aoValidationKitRows += [ ( 'Product:', oValidationKitEx.oCat.sProduct ), ];
+ if oValidationKitEx.oCat.sBranch != 'trunk':
+ aoValidationKitRows += [ ( 'Product:', oValidationKitEx.oCat.sBranch ), ];
+ if oValidationKitEx.oCat.sType != 'release':
+ aoValidationKitRows += [ ( 'Type:', oValidationKitEx.oCat.sType), ];
+ if oValidationKitEx.sVersion != '0.0.0':
+ aoValidationKitRows += [ ( 'Version:', oValidationKitEx.sVersion ), ];
+ aoValidationKitRows += [
+ ( 'Created:', oValidationKitEx.tsCreated ),
+ ];
+ if oValidationKitEx.uidAuthor is not None:
+ aoValidationKitRows += [ ( 'Author ID:', oValidationKitEx.uidAuthor ), ];
+ if oValidationKitEx.sLogUrl is not None:
+ aoValidationKitRows += [ ( 'Log:', WuiBuildLogLink(oValidationKitEx.sLogUrl, fBracketed = False) ), ];
+
+ # TestBox.
+ aoTestBoxRows = [
+ WuiHtmlKeeper([ WuiTmLink(oTestBox.sName, self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestBoxDetails,
+ TestBoxData.ksParam_idGenTestBox: oTestSet.idGenTestBox, },
+ fBracketed = False),
+ WuiTestResultsForTestBoxLink(oTestBox.idTestBox),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oTestSet.idTestBox,
+ tsNow = tsReportEffectiveDate, fBracketed = False),
+ ]),
+ ];
+ if oTestBox.sDescription:
+ aoTestBoxRows.append([oTestBox.sDescription, ]);
+ aoTestBoxRows += [
+ ( 'IP:', oTestBox.ip ),
+ #( 'UUID:', oTestBox.uuidSystem ),
+ #( 'Enabled:', oTestBox.fEnabled ),
+ #( 'Lom Kind:', oTestBox.enmLomKind ),
+ #( 'Lom IP:', oTestBox.ipLom ),
+ ( 'OS/Arch:', '%s.%s' % (oTestBox.sOs, oTestBox.sCpuArch) ),
+ ( 'OS Version:', oTestBox.sOsVersion ),
+ ( 'CPUs:', oTestBox.cCpus ),
+ ];
+ if oTestBox.sCpuName is not None:
+ aoTestBoxRows.append(['CPU Name', oTestBox.sCpuName.replace(' ', ' ')]);
+ if oTestBox.lCpuRevision is not None:
+ sMarch = oTestBox.queryCpuMicroarch();
+ if sMarch is not None:
+ aoTestBoxRows.append( ('CPU Microarch', sMarch) );
+ uFamily = oTestBox.getCpuFamily();
+ uModel = oTestBox.getCpuModel();
+ uStepping = oTestBox.getCpuStepping();
+ aoTestBoxRows += [
+ ( 'CPU Family', '%u (%#x)' % ( uFamily, uFamily, ) ),
+ ( 'CPU Model', '%u (%#x)' % ( uModel, uModel, ) ),
+ ( 'CPU Stepping', '%u (%#x)' % ( uStepping, uStepping, ) ),
+ ];
+ asFeatures = [ oTestBox.sCpuVendor, ];
+ if oTestBox.fCpuHwVirt is True: asFeatures.append(u'HW\u2011Virt');
+ if oTestBox.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging');
+ if oTestBox.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest');
+ if oTestBox.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU');
+ aoTestBoxRows += [
+ ( 'Features:', u' '.join(asFeatures) ),
+ ( 'RAM size:', '%s MB' % (oTestBox.cMbMemory,) ),
+ ( 'Scratch Size:', '%s MB' % (oTestBox.cMbScratch,) ),
+ ( 'Scale Timeout:', '%s%%' % (oTestBox.pctScaleTimeout,) ),
+ ( 'Script Rev:', WuiSvnLink(oTestBox.iTestBoxScriptRev, fBracketed = False) ),
+ ( 'Python:', oTestBox.formatPythonVersion() ),
+ ( 'Pending Command:', oTestBox.enmPendingCmd ),
+ ];
+
+ aoRows = [
+ aoResultRows,
+ aoBuildRows,
+ aoValidationKitRows,
+ aoTestBoxRows,
+ ];
+
+ asHtml.append(self._htmlTable(aoRows));
+
+ #
+ # Convert the tree to a list of events, values, message and files.
+ #
+ sHtmlEvents = '';
+ sHtmlEvents += '<table class="tmtbl-events" id="tmtbl-events" width="100%">\n';
+ sHtmlEvents += ' <tr class="tmheader">\n' \
+ ' <th>When</th>\n' \
+ ' <th></th>\n' \
+ ' <th>Elapsed</th>\n' \
+ ' <th>Event name</th>\n' \
+ ' <th colspan="2">Value (status)</th>' \
+ ' <th></th>\n' \
+ ' </tr>\n';
+ sPrettyCmdLine = '&nbsp;\\<br>&nbsp;&nbsp;&nbsp;&nbsp;\n'.join(webutils.escapeElem(oTestCaseEx.sBaseCmd
+ + ' '
+ + oTestVarEx.sArgs).split() );
+ (sTmp, _, cFailures) = self._recursivelyGenerateEvents(oTestResultTree, sPrettyCmdLine, '', 1, 0, oTestSet, 0);
+ sHtmlEvents += sTmp;
+
+ sHtmlEvents += '</table>\n'
+
+ #
+ # Put it all together.
+ #
+ sHtml = '<table class="tmtbl-testresult-details-base" width="100%">\n';
+ sHtml += ' <tr>\n'
+ sHtml += ' <td valign="top" width="20%%">\n%s\n</td>\n' % ' <br>\n'.join(asHtml);
+
+ sHtml += ' <td valign="top" width="80%" style="padding-left:6px">\n';
+ sHtml += self._generateMainReason(oTestResultTree, oTestSet);
+
+ sHtml += ' <h2>Events:</h2>\n';
+ sHtml += ' <form action="#" method="get" id="graph-form">\n' \
+ ' <input type="hidden" name="%s" value="%s"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ % ( WuiMain.ksParamAction, WuiMain.ksActionGraphWiz,
+ WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox,
+ WuiMain.ksParamGraphWizBuildCatIds, oBuildEx.idBuildCategory,
+ WuiMain.ksParamGraphWizTestCaseIds, oTestSet.idTestCase,
+ WuiMain.ksParamGraphWizSrcTestSetId, oTestSet.idTestSet,
+ );
+ if oTestSet.tsDone is not None:
+ sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \
+ % ( WuiMain.ksParamEffectiveDate, oTestSet.tsDone, );
+ sHtml += ' <p>\n';
+ sFormButton = '<button type="submit" onclick="%s">Show graphs</button>' \
+ % ( webutils.escapeAttr('addDynamicGraphInputs("graph-form", "main", "%s", "%s");'
+ % (WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizDpi, )) );
+ sHtml += ' ' + sFormButton + '\n';
+ sHtml += ' %s %s %s\n' \
+ % ( WuiTmLink('Log File', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionViewLog,
+ WuiMain.ksParamLogSetId: oTestSet.idTestSet,
+ }),
+ WuiTmLink('Raw Log', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionGetFile,
+ WuiMain.ksParamGetFileSetId: oTestSet.idTestSet,
+ WuiMain.ksParamGetFileDownloadIt: False,
+ }),
+ WuiTmLink('Download Log', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionGetFile,
+ WuiMain.ksParamGetFileSetId: oTestSet.idTestSet,
+ WuiMain.ksParamGetFileDownloadIt: True,
+ }),
+ );
+ sHtml += ' </p>\n';
+ if cFailures == 1:
+ sHtml += ' <p>%s</p>\n' % ( WuiTmLink('Jump to failure', '#failure-0'), )
+ elif cFailures > 1:
+ sHtml += ' <p>Jump to failure: ';
+ if cFailures <= 13:
+ for iFailure in range(0, cFailures):
+ sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml();
+ else:
+ for iFailure in range(0, 6):
+ sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml();
+ sHtml += ' ... ';
+ for iFailure in range(cFailures - 6, cFailures):
+ sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml();
+ sHtml += ' </p>\n';
+
+ sHtml += sHtmlEvents;
+ sHtml += ' <p>' + sFormButton + '</p>\n';
+ sHtml += ' </form>\n';
+ sHtml += ' </td>\n';
+
+ sHtml += ' </tr>\n';
+ sHtml += '</table>\n';
+
+ return ('Test Case result details', sHtml)
+
+
+class WuiGroupedResultList(WuiListContentBase):
+ """
+ WUI results content generator.
+ """
+
+ def __init__(self, aoEntries, cEntriesCount, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp,
+ aiSelectedSortColumns = None):
+ """Override initialization"""
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Ungrouped (%d)' % cEntriesCount, sId = 'results',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._cEntriesCount = cEntriesCount
+
+ self._asColumnHeaders = [
+ 'Start',
+ 'Product Build',
+ 'Kit',
+ 'Box',
+ 'OS.Arch',
+ 'Test Case',
+ 'Elapsed',
+ 'Result',
+ 'Reason',
+ ];
+ self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', ];
+
+
+ # Prepare parameter lists.
+ self._dTestBoxLinkParams = self._oDisp.getParameters();
+ self._dTestBoxLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByTestBox;
+
+ self._dTestCaseLinkParams = self._oDisp.getParameters();
+ self._dTestCaseLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByTestCase;
+
+ self._dRevLinkParams = self._oDisp.getParameters();
+ self._dRevLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByBuildRev;
+
+
+
+ def _formatListEntry(self, iEntry):
+ """
+ Format *show all* table entry
+ """
+ oEntry = self._aoEntries[iEntry];
+
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ from testmanager.webui.wuireport import WuiReportSummaryLink;
+
+ oValidationKit = None;
+ if oEntry.idBuildTestSuite is not None:
+ oValidationKit = WuiTmLink('r%s' % (oEntry.iRevisionTestSuite,),
+ WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oEntry.idBuildTestSuite },
+ fBracketed = False);
+
+ aoTestSetLinks = [];
+ aoTestSetLinks.append(WuiTmLink(oEntry.enmStatus,
+ WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: oEntry.idTestSet },
+ fBracketed = False));
+ if oEntry.cErrors > 0:
+ aoTestSetLinks.append(WuiRawHtml('-'));
+ aoTestSetLinks.append(WuiTmLink('%d error%s' % (oEntry.cErrors, '' if oEntry.cErrors == 1 else 's', ),
+ WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: oEntry.idTestSet },
+ sFragmentId = 'failure-0', fBracketed = False));
+
+
+ self._dTestBoxLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.idTestBox;
+ self._dTestCaseLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.idTestCase;
+ self._dRevLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.iRevision;
+
+ sTestBoxTitle = u'';
+ if oEntry.sCpuVendor is not None:
+ sTestBoxTitle += 'CPU vendor:\t%s\n' % ( oEntry.sCpuVendor, );
+ if oEntry.sCpuName is not None:
+ sTestBoxTitle += 'CPU name:\t%s\n' % ( ' '.join(oEntry.sCpuName.split()), );
+ if oEntry.sOsVersion is not None:
+ sTestBoxTitle += 'OS version:\t%s\n' % ( oEntry.sOsVersion, );
+ asFeatures = [];
+ if oEntry.fCpuHwVirt is True:
+ if oEntry.sCpuVendor is None:
+ asFeatures.append(u'HW\u2011Virt');
+ elif oEntry.sCpuVendor in ['AuthenticAMD',]:
+ asFeatures.append(u'HW\u2011Virt(AMD\u2011V)');
+ else:
+ asFeatures.append(u'HW\u2011Virt(VT\u2011x)');
+ if oEntry.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging');
+ if oEntry.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest');
+ #if oEntry.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU');
+ sTestBoxTitle += u'CPU features:\t' + u', '.join(asFeatures);
+
+ # Testcase
+ if oEntry.sSubName:
+ sTestCaseName = '%s / %s' % (oEntry.sTestCaseName, oEntry.sSubName,);
+ else:
+ sTestCaseName = oEntry.sTestCaseName;
+
+ # Reason:
+ aoReasons = [];
+ for oIt in oEntry.aoFailureReasons:
+ sReasonTitle = 'Reason: \t%s\n' % ( oIt.oFailureReason.sShort, );
+ sReasonTitle += 'Category:\t%s\n' % ( oIt.oFailureReason.oCategory.sShort, );
+ sReasonTitle += 'Assigned:\t%s\n' % ( self.formatTsShort(oIt.tsFailureReasonAssigned), );
+ sReasonTitle += 'By User: \t%s\n' % ( oIt.oFailureReasonAssigner.sUsername, );
+ if oIt.sFailureReasonComment:
+ sReasonTitle += 'Comment: \t%s\n' % ( oIt.sFailureReasonComment, );
+ if oIt.oFailureReason.iTicket is not None and oIt.oFailureReason.iTicket > 0:
+ sReasonTitle += 'xTracker:\t#%s\n' % ( oIt.oFailureReason.iTicket, );
+ for i, sUrl in enumerate(oIt.oFailureReason.asUrls):
+ sUrl = sUrl.strip();
+ if sUrl:
+ sReasonTitle += 'URL#%u: \t%s\n' % ( i, sUrl, );
+ aoReasons.append(WuiTmLink(oIt.oFailureReason.sShort, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
+ FailureReasonData.ksParam_idFailureReason: oIt.oFailureReason.idFailureReason },
+ sTitle = sReasonTitle));
+
+ return [
+ oEntry.tsCreated,
+ [ WuiTmLink('%s %s (%s)' % (oEntry.sProduct, oEntry.sVersion, oEntry.sType,),
+ WuiMain.ksScriptName, self._dRevLinkParams, sTitle = '%s' % (oEntry.sBranch,), fBracketed = False),
+ WuiSvnLinkWithTooltip(oEntry.iRevision, 'vbox'), ## @todo add sRepository TestResultListingData
+ WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oEntry.idBuild },
+ fBracketed = False),
+ ],
+ oValidationKit,
+ [ WuiTmLink(oEntry.sTestBoxName, WuiMain.ksScriptName, self._dTestBoxLinkParams, fBracketed = False,
+ sTitle = sTestBoxTitle),
+ WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails,
+ TestBoxData.ksParam_idTestBox: oEntry.idTestBox },
+ fBracketed = False),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oEntry.idTestBox, fBracketed = False), ],
+ '%s.%s' % (oEntry.sOs, oEntry.sArch),
+ [ WuiTmLink(sTestCaseName, WuiMain.ksScriptName, self._dTestCaseLinkParams, fBracketed = False,
+ sTitle = (oEntry.sBaseCmd + ' ' + oEntry.sArgs) if oEntry.sArgs else oEntry.sBaseCmd),
+ WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idTestCase: oEntry.idTestCase },
+ fBracketed = False),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oEntry.idTestCase, fBracketed = False), ],
+ oEntry.tsElapsed,
+ aoTestSetLinks,
+ aoReasons
+ ];
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py b/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py
new file mode 100755
index 00000000..9ffd5518
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+# $Id: wuitestresultfailure.py $
+
+"""
+Test Manager WUI - Dummy Test Result Failure Reason Edit Dialog - just for error handling!
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiContentBase, WuiTmLink;
+from testmanager.webui.wuimain import WuiMain;
+from testmanager.webui.wuiadminfailurereason import WuiFailureReasonDetailsLink, WuiFailureReasonAddLink;
+from testmanager.core.testresultfailures import TestResultFailureData;
+from testmanager.core.testset import TestSetData;
+from testmanager.core.failurereason import FailureReasonLogic;
+
+
+
+class WuiTestResultFailureDetailsLink(WuiTmLink):
+ """ Link for adding a failure reason. """
+ def __init__(self, idTestResult, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
+ if fBracketed is None:
+ fBracketed = len(sName) > 2;
+ WuiTmLink.__init__(self, sName = sName,
+ sUrlBase = WuiMain.ksScriptName,
+ dParams = { WuiMain.ksParamAction: WuiMain.ksActionTestResultFailureDetails,
+ TestResultFailureData.ksParam_idTestResult: idTestResult, },
+ fBracketed = fBracketed,
+ sTitle = sTitle);
+ self.idTestResult = idTestResult;
+
+
+
+class WuiTestResultFailure(WuiFormContentBase):
+ """
+ WUI test result failure error form generator.
+ """
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add Test Result Failure Reason';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Modify Test Result Failure Reason';
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Test Result Failure Reason';
+ ## submit access.
+ WuiFormContentBase.__init__(self, oData, sMode, 'TestResultFailure', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+
+ aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Todo: Figure out why');
+ sPostHtml = '';
+ if oData.idFailureReason is not None and oData.idFailureReason >= 0:
+ sPostHtml += u' ' + WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml();
+ sPostHtml += u' ' + WuiFailureReasonAddLink('New', fBracketed = False).toHtml();
+ oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason,
+ 'Reason', aoFailureReasons, sPostHtml = sPostHtml);
+ oForm.addMultilineText(TestResultFailureData.ksParam_sComment, oData.sComment, 'Comment');
+ oForm.addIntRO( TestResultFailureData.ksParam_idTestResult, oData.idTestResult, 'Test Result ID');
+ oForm.addIntRO( TestResultFailureData.ksParam_idTestSet, oData.idTestSet, 'Test Set ID');
+ oForm.addTimestampRO(TestResultFailureData.ksParam_tsEffective, oData.tsEffective, 'Effective Date');
+ oForm.addTimestampRO(TestResultFailureData.ksParam_tsExpire, oData.tsExpire, 'Expire (excl)');
+ oForm.addIntRO( TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID');
+ if self._sMode != WuiFormContentBase.ksMode_Show:
+ oForm.addSubmit('Add' if self._sMode == WuiFormContentBase.ksMode_Add else 'Modify');
+ return True;
+
+ def _generateTopRowFormActions(self, oData):
+ """
+ We add a way to get back to the test set to the actions.
+ """
+ aoActions = super(WuiTestResultFailure, self)._generateTopRowFormActions(oData);
+ if oData and oData.idTestResult and int(oData.idTestResult) > 0:
+ aoActions.append(WuiTmLink('Associated Test Set', WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionTestSetDetailsFromResult,
+ TestSetData.ksParam_idTestResult: oData.idTestResult }
+ ));
+ return aoActions;
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py b/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py
new file mode 100755
index 00000000..1df9bbb2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# $Id: wuivcshistory.py $
+
+"""
+Test Manager WUI - VCS history
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Python imports.
+#import datetime;
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core import db;
+from testmanager.webui.wuicontentbase import WuiContentBase;
+from common import webutils;
+
+class WuiVcsHistoryTooltip(WuiContentBase):
+ """
+ WUI VCS history tooltip generator.
+ """
+
+ def __init__(self, aoEntries, sRepository, iRevision, cEntries, fnDPrint, oDisp):
+ """Override initialization"""
+ WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
+ self.aoEntries = aoEntries;
+ self.sRepository = sRepository;
+ self.iRevision = iRevision;
+ self.cEntries = cEntries;
+
+
+ def show(self):
+ """
+ Generates the tooltip.
+ Returns (sTitle, HTML).
+ """
+ sHtml = '<div class="tmvcstimeline tmvcstimelinetooltip">\n';
+
+ oCurDate = None;
+ for oEntry in self.aoEntries:
+ oTsZulu = db.dbTimestampToZuluDatetime(oEntry.tsCreated);
+ if oCurDate is None or oCurDate != oTsZulu.date():
+ if oCurDate is not None:
+ sHtml += ' </dl>\n'
+ oCurDate = oTsZulu.date();
+ sHtml += ' <h2>%s:</h2>\n' \
+ ' <dl>\n' \
+ % (oTsZulu.strftime('%Y-%m-%d'),);
+
+ sEntry = ' <dt id="r%s">' % (oEntry.iRevision, );
+ sEntry += '<a href="%s" target="_blank">' \
+ % ( webutils.escapeAttr(config.g_ksTracChangsetUrlFmt
+ % { 'iRevision': oEntry.iRevision, 'sRepository': oEntry.sRepository,}), );
+
+ sEntry += '<span class="tmvcstimeline-time">%s</span>' % ( oTsZulu.strftime('%H:%MZ'), );
+ sEntry += ' Changeset <span class="tmvcstimeline-rev">[%s]</span>' % ( oEntry.iRevision, );
+ sEntry += ' by <span class="tmvcstimeline-author">%s</span>' % ( webutils.escapeElem(oEntry.sAuthor), );
+ sEntry += '</a>\n';
+ sEntry += '</dt>\n';
+ sEntry += ' <dd>%s</dd>\n' % ( webutils.escapeElem(oEntry.sMessage), );
+
+ sHtml += sEntry;
+
+ if oCurDate is not None:
+ sHtml += ' </dl>\n';
+ sHtml += '</div>\n';
+
+ return ('VCS History Tooltip', sHtml);
+