This patchset provides support for tracking known failures in the TestBot results. The main goal is to avoid false positives but there are some side benefits as well: * The job reports sent to developers link to the relevant WineHQ bugs which gives them more exposure and may help get them fixed faster. * The failure details pages will eventually link to all the tasks where the failure happened, which could be valuable to find patterns. * The failure page tracks when it was last seen. This simplifies spotting bugs which are fixed and could be closed. (Or failures where the regular expression needs to be updated / fixed)
The patchset mostly follows the scheme described in bug 48912: https://bugs.winehq.org/show_bug.cgi?id=48912
There are some differences though: * The bug envisionned this scheme purely as a blacklist to avoid false positives. The implementation can certainly be used in this way but it is not limited to failures that cause false positives. Tracking other failures can provide the side benefits mentionned above. The only limit is the maintenance work required to track more failures.
* As a consequence the FailureBlacklist class was renamed to Failure, and the FailureBlacklistUses was renamed to TaskFailures since it links Tasks with the related Failures.
* The FailureBlacklistVMs purpose is not really described in the bug but I think the goal was to provide a way to deckare that the failure is expected to only happen on specific test configurations. Instead the Failures class has a ConfigRegExp regular expression which specifies which test configurations the failure applies to. The test 'configuration name' is of the form 'VM:logfile' which allows matching specific Windows versions (thanks to the TestBot VM naming scheme), specific Windows locales (part of the VM name), specific bitnesses (part of the log filename), or specific Wine locales (part of the log filename). For instance: ^w8 -> all Windows 8 VMs :exe -> all Windows tests (exe32.report and exe64.report) ^w10.*_ja.*: -> all Windows 10 Japanese test configurations win|wow -> all Wine test configurations (win32.report, ...) (win|wow).*_fr -> French Wine test configurations
* Changes in FailureBlacklists -> Failures - Primary key: (Bug, TestModule, TestUnit) -> Id Some issues generate quite a few failures. So in case trying to match them all results in a regular expression that's too long (either because of the database limit or for readability), this change makes it possible to add multiple Failure objects to track them all.
- TestModule -> ErrorGroup The .errors files split the errors in groups. Each group corresponds to a test module, but extra errors (too much output, missing summary lines, etc) are in a separate group. So I renamed TestModule to ErrorGroup as a reminder to the coder that this is meant to match the .errors group names, not just test modules.
- Name -> (BugStatus, BugDescription) Instead of having a name describing the failure, this patchset uses the corresponding WineHQ bug description. The TestBot also retrieves the bug status which allows checking that closed bugs dont happen anymore, or open bugs that should be closed.
- LastUsed -> (LastNew, LastOld) The LastUse field was split into LastNew and LastOld to track whether the failure would cause false positives without the known failures support.
* Changes in FailureBlacklistUses -> TaskFailures - (Bug, TestModule, TestUnit) -> FailureId This is a consequence of the Failure primary key change described above.
- () -> (TaskLog) This allows tracking which task log the failure was found in. This is quite important for Wine tasks since they run all the test configurations in just one task.
- () -> (NewCount, OldCount) This provides a count of matching failures and whether they would have been tagged as old or new without this patchset. This provides a quick way to know whether there is the same number of failures every time or whether the number of failures changes from one run to the next. It also provides a quick way to know how many failures to look for when jumping to the log.
Francois Gouget (6): testbot: Add tables to track the test failures. testbot/web: Add pages to add, edit and show known failures. testbot: Match the failures with the test logs. testbot/web: Link job details to the related known failures. testbot/WineSendLog: Take into account the known failures. testbot/UpdateFailures: Add a script to refresh the failures' bug information.
testbot/bin/UpdateFailures.pl | 192 ++++++ testbot/bin/WineSendLog.pl | 78 ++- testbot/ddl/update47.sql | 33 + testbot/ddl/winetestbot.sql | 32 + testbot/doc/INSTALL.txt | 5 + testbot/doc/winetestbot-schema.dia | 852 +++++++++++++++++++++--- testbot/lib/WineTestBot/CGI/PageBase.pm | 1 + testbot/lib/WineTestBot/Config.pm | 6 +- testbot/lib/WineTestBot/Failures.pm | 184 +++++ testbot/lib/WineTestBot/Jobs.pm | 6 + testbot/lib/WineTestBot/LogUtils.pm | 180 ++++- testbot/lib/WineTestBot/TaskFailures.pm | 120 ++++ testbot/lib/WineTestBot/Tasks.pm | 4 + testbot/web/FailureDetails.pl | 119 ++++ testbot/web/FailuresList.pl | 160 +++++ testbot/web/JobDetails.pl | 54 +- testbot/web/WineTestBot.css | 3 + 17 files changed, 1907 insertions(+), 122 deletions(-) create mode 100755 testbot/bin/UpdateFailures.pl create mode 100644 testbot/ddl/update47.sql create mode 100644 testbot/lib/WineTestBot/Failures.pm create mode 100644 testbot/lib/WineTestBot/TaskFailures.pm create mode 100644 testbot/web/FailureDetails.pl create mode 100644 testbot/web/FailuresList.pl
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=48912 Signed-off-by: Francois Gouget fgouget@codeweavers.com --- This patch requires updating the database schema and restarting the TestBot Engine and web server.
Notes: - This first patch only introduces the core infrastructure. The other parts are: - Providing a way to enter known failures. - Matching the known failures in the logs and recording the matches in the database. - Taking the known failures into account in the report emails and patches status. - Identifying the known failures on the JobDetails page. - Update the associated bug information. - Most bug tracking systems use integers to identify bugs. If we ever want to move Wine's bug tracking away from Bugzilla we may have to change the type of the BugId field. We may also need a way to know which bug tracker a given BugId refers to which may involve adding a BugTracker field or replacing the BugId field with the whole bug URL. - The Jobs::Restart() chunk depends on the patch that turns Itemrefs into virtual properties. Without it it would be unable to filter the TaskFailures table on the JobId property. --- testbot/ddl/update47.sql | 33 + testbot/ddl/winetestbot.sql | 32 + testbot/doc/winetestbot-schema.dia | 852 +++++++++++++++++++++--- testbot/lib/WineTestBot/Failures.pm | 184 +++++ testbot/lib/WineTestBot/Jobs.pm | 6 + testbot/lib/WineTestBot/TaskFailures.pm | 120 ++++ testbot/lib/WineTestBot/Tasks.pm | 4 + 7 files changed, 1142 insertions(+), 89 deletions(-) create mode 100644 testbot/ddl/update47.sql create mode 100644 testbot/lib/WineTestBot/Failures.pm create mode 100644 testbot/lib/WineTestBot/TaskFailures.pm
diff --git a/testbot/ddl/update47.sql b/testbot/ddl/update47.sql new file mode 100644 index 000000000..177eadc28 --- /dev/null +++ b/testbot/ddl/update47.sql @@ -0,0 +1,33 @@ +USE winetestbot; + +CREATE TABLE Failures +( + Id INT NOT NULL AUTO_INCREMENT, + ErrorGroup VARCHAR(64) NULL, + TestUnit VARCHAR(32) NULL, + ConfigRegExp VARCHAR(64) NULL, + FailureRegExp VARCHAR(256) NOT NULL, + Notes VARCHAR(128) NULL, + LastNew DATETIME NULL, + LastOld DATETIME NULL, + BugId INT NOT NULL, + BugStatus VARCHAR(32) NULL, + BugDescription VARCHAR(128) NULL, + PRIMARY KEY (Id) +) +ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE TaskFailures +( + FailureId INT NOT NULL, + JobId INT NOT NULL, + StepNo INT(2) NOT NULL, + TaskNo INT(2) NOT NULL, + TaskLog VARCHAR(32) NOT NULL, + NewCount INT NULL, + OldCount INT NULL, + FOREIGN KEY(FailureId) REFERENCES Failures(Id), + FOREIGN KEY(JobId, StepNo, TaskNo) REFERENCES Tasks(JobId, StepNo, No), + PRIMARY KEY (FailureId, JobId, StepNo, TaskNo, TaskLog) +) +ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/testbot/ddl/winetestbot.sql b/testbot/ddl/winetestbot.sql index 2d085247c..0cf70484c 100644 --- a/testbot/ddl/winetestbot.sql +++ b/testbot/ddl/winetestbot.sql @@ -164,6 +164,38 @@ CREATE TABLE Tasks ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE Failures +( + Id INT NOT NULL AUTO_INCREMENT, + ErrorGroup VARCHAR(64) NULL, + TestUnit VARCHAR(32) NULL, + ConfigRegExp VARCHAR(64) NULL, + FailureRegExp VARCHAR(256) NOT NULL, + Notes VARCHAR(128) NULL, + LastNew DATETIME NULL, + LastOld DATETIME NULL, + BugId INT NOT NULL, + BugStatus VARCHAR(32) NULL, + BugDescription VARCHAR(128) NULL, + PRIMARY KEY (Id) +) +ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE TaskFailures +( + FailureId INT NOT NULL, + JobId INT NOT NULL, + StepNo INT(2) NOT NULL, + TaskNo INT(2) NOT NULL, + TaskLog VARCHAR(32) NOT NULL, + NewCount INT NULL, + OldCount INT NULL, + FOREIGN KEY(FailureId) REFERENCES Failures(Id), + FOREIGN KEY(JobId, StepNo, TaskNo) REFERENCES Tasks(JobId, StepNo, No), + PRIMARY KEY (FailureId, JobId, StepNo, TaskNo, TaskLog) +) +ENGINE=InnoDB DEFAULT CHARSET=utf8; + CREATE TABLE RecordGroups ( Id INT NOT NULL AUTO_INCREMENT, diff --git a/testbot/doc/winetestbot-schema.dia b/testbot/doc/winetestbot-schema.dia index b2a686285..effbb9a82 100644 --- a/testbot/doc/winetestbot-schema.dia +++ b/testbot/doc/winetestbot-schema.dia @@ -669,7 +669,7 @@ <dia:point val="-7.87083,-21.5"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-7.92083,-23;-4.3375,-21.45"/> + <dia:rectangle val="-7.92083,-22.9955;-4.3375,-21.45"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> @@ -729,7 +729,7 @@ <dia:point val="4.6125,-22.35"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="4.5625,-23;11.0958,-22.2834"/> + <dia:rectangle val="4.5625,-22.9955;11.0958,-22.2834"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> @@ -789,7 +789,7 @@ <dia:point val="19.6608,-21.5334"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="19.6108,-22.2;25.4458,-21.4834"/> + <dia:rectangle val="19.6108,-22.1955;25.4458,-21.4834"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> @@ -846,16 +846,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O7"> <dia:attribute name="obj_pos"> - <dia:point val="-16.8208,-13.3167"/> + <dia:point val="-16.8208,-6.6667"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-16.8208,-13.3167;-7.8208,-10.3167"/> + <dia:rectangle val="-16.8208,-6.6667;-7.8208,-3.6667"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="-16.8208,-13.3167"/> + <dia:point val="-16.8208,-6.6667"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="9"/> @@ -962,16 +962,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O8"> <dia:attribute name="obj_pos"> - <dia:point val="-16.3208,-2.55002"/> + <dia:point val="-16.3208,3.89998"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-16.3208,-2.55002;-6.1658,5.24998"/> + <dia:rectangle val="-16.3208,3.89998;-6.1658,11.7"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="-16.3208,-2.55002"/> + <dia:point val="-16.3208,3.89998"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="10.155000000000001"/> @@ -1216,16 +1216,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O9"> <dia:attribute name="obj_pos"> - <dia:point val="-4.57083,-14.9167"/> + <dia:point val="-4.57083,-8.2667"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-4.57083,-14.9167;4.81417,-6.3167"/> + <dia:rectangle val="-4.57083,-8.2667;4.81417,0.3333"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="-4.57083,-14.9167"/> + <dia:point val="-4.57083,-8.2667"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="9.3850000000000016"/> @@ -1493,16 +1493,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O10"> <dia:attribute name="obj_pos"> - <dia:point val="-2.55417,-2.51669"/> + <dia:point val="-2.55417,3.88331"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-2.55417,-2.51669;12.6058,2.88331"/> + <dia:rectangle val="-2.55417,3.88331;12.6058,9.28331"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="-2.55417,-2.51669"/> + <dia:point val="-2.55417,3.88331"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="15.16"/> @@ -1678,16 +1678,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O11"> <dia:attribute name="obj_pos"> - <dia:point val="8.24583,-14.8834"/> + <dia:point val="8.19583,-8.2334"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="8.24583,-14.8834;22.2508,-6.2834"/> + <dia:rectangle val="8.19583,-8.2334;22.2008,0.3666"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="8.24583,-14.8834"/> + <dia:point val="8.19583,-8.2334"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="14.004999999999999"/> @@ -1955,16 +1955,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O12"> <dia:attribute name="obj_pos"> - <dia:point val="25.4792,-14.8834"/> + <dia:point val="25.4792,-8.2334"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="25.4792,-14.8834;36.0192,-4.6834"/> + <dia:rectangle val="25.4792,-8.2334;36.0192,1.9666"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="25.4792,-14.8834"/> + <dia:point val="25.4792,-8.2334"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="10.539999999999999"/> @@ -2278,16 +2278,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O13"> <dia:attribute name="obj_pos"> - <dia:point val="25.6708,-2.02919"/> + <dia:point val="25.6708,3.87081"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="25.6708,-2.02919;36.5958,12.1708"/> + <dia:rectangle val="25.6708,3.87081;36.5958,18.0708"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="25.6708,-2.02919"/> + <dia:point val="25.6708,3.87081"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="10.925000000000001"/> @@ -2716,19 +2716,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O14"> <dia:attribute name="obj_pos"> - <dia:point val="-7.8208,-11.6167"/> + <dia:point val="-7.8208,-4.9667"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-7.8708,-12.2667;-4.52083,-11.5667"/> + <dia:rectangle val="-7.8708,-5.61225;-4.52083,-4.9167"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="-7.8208,-11.6167"/> - <dia:point val="-5.92083,-11.6167"/> - <dia:point val="-5.92083,-11.6167"/> - <dia:point val="-4.57083,-11.6167"/> + <dia:point val="-7.8208,-4.9667"/> + <dia:point val="-5.92083,-4.9667"/> + <dia:point val="-5.92083,-4.9667"/> + <dia:point val="-4.57083,-4.9667"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -2776,19 +2776,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O15"> <dia:attribute name="obj_pos"> - <dia:point val="4.81417,-13.2167"/> + <dia:point val="4.81417,-6.5667"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="4.76417,-13.8667;8.29583,-13.1334"/> + <dia:rectangle val="4.76417,-7.21225;8.24583,-6.4834"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="4.81417,-13.2167"/> - <dia:point val="6.4625,-13.2167"/> - <dia:point val="6.4625,-13.1834"/> - <dia:point val="8.24583,-13.1834"/> + <dia:point val="4.81417,-6.5667"/> + <dia:point val="6.4625,-6.5667"/> + <dia:point val="6.4625,-6.5334"/> + <dia:point val="8.19583,-6.5334"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -2836,19 +2836,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O16"> <dia:attribute name="obj_pos"> - <dia:point val="22.2508,-12.3834"/> + <dia:point val="22.2008,-5.7334"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="22.2008,-13.0334;25.5292,-12.3334"/> + <dia:rectangle val="22.1508,-6.37895;25.5292,-5.6834"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="22.2508,-12.3834"/> - <dia:point val="23.7334,-12.3834"/> - <dia:point val="23.7334,-12.3834"/> - <dia:point val="25.4792,-12.3834"/> + <dia:point val="22.2008,-5.7334"/> + <dia:point val="23.7334,-5.7334"/> + <dia:point val="23.7334,-5.7334"/> + <dia:point val="25.4792,-5.7334"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -2896,19 +2896,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O17"> <dia:attribute name="obj_pos"> - <dia:point val="36.0192,-9.98336"/> + <dia:point val="36.0192,-3.3334"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="35.9692,-10.6334;37.6693,-0.27919"/> + <dia:rectangle val="35.9692,-3.97895;37.6693,5.62081"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="36.0192,-9.98336"/> - <dia:point val="37.6193,-9.98336"/> - <dia:point val="37.6193,-0.32919"/> - <dia:point val="36.5958,-0.32919"/> + <dia:point val="36.0192,-3.3334"/> + <dia:point val="37.6193,-3.3334"/> + <dia:point val="37.6193,5.57081"/> + <dia:point val="36.5958,5.57081"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -2959,7 +2959,7 @@ <dia:point val="0.1125,-17.85"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="0.0625,-17.9;1.48167,-14.8667"/> + <dia:rectangle val="0.0625,-17.9;1.48167,-8.2167"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> @@ -2968,7 +2968,7 @@ <dia:point val="0.1125,-17.85"/> <dia:point val="0.1125,-15.3584"/> <dia:point val="0.12167,-15.3584"/> - <dia:point val="0.12167,-14.9167"/> + <dia:point val="0.12167,-8.2667"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="1"/> @@ -3016,19 +3016,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O19"> <dia:attribute name="obj_pos"> - <dia:point val="-4.57083,-6.8167"/> + <dia:point val="-4.57083,-0.1667"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-18.2807,-7.4667;-4.52083,-0.80002"/> + <dia:rectangle val="-18.2807,-0.812247;-4.52083,5.64998"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="-4.57083,-6.8167"/> - <dia:point val="-18.2307,-6.8167"/> - <dia:point val="-18.2307,-0.85002"/> - <dia:point val="-16.3208,-0.85002"/> + <dia:point val="-4.57083,-0.1667"/> + <dia:point val="-18.2307,-0.1667"/> + <dia:point val="-18.2307,5.59998"/> + <dia:point val="-16.3208,5.59998"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -3076,19 +3076,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O20"> <dia:attribute name="obj_pos"> - <dia:point val="-6.1658,-0.85002"/> + <dia:point val="-6.1658,5.59998"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="-6.2158,-1.50002;-2.50417,1.63331"/> + <dia:rectangle val="-6.2158,4.95443;-2.50417,8.03331"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="-6.1658,-0.85002"/> - <dia:point val="-4.29249,-0.85002"/> - <dia:point val="-4.29249,1.58331"/> - <dia:point val="-2.55417,1.58331"/> + <dia:point val="-6.1658,5.59998"/> + <dia:point val="-4.29249,5.59998"/> + <dia:point val="-4.29249,7.98331"/> + <dia:point val="-2.55417,7.98331"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -3136,19 +3136,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O21"> <dia:attribute name="obj_pos"> - <dia:point val="12.6058,-0.81669"/> + <dia:point val="12.6058,5.58331"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="12.5558,-1.50002;15.1193,-0.76669"/> + <dia:rectangle val="12.5558,4.93776;15.1193,5.64998"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="12.6058,-0.81669"/> - <dia:point val="13.7696,-0.81669"/> - <dia:point val="13.7696,-0.85002"/> - <dia:point val="15.0693,-0.85002"/> + <dia:point val="12.6058,5.58331"/> + <dia:point val="13.7696,5.58331"/> + <dia:point val="13.7696,5.59998"/> + <dia:point val="15.0693,5.59998"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -3196,16 +3196,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O22"> <dia:attribute name="obj_pos"> - <dia:point val="15.0693,-2.55002"/> + <dia:point val="15.0693,3.89998"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="15.0693,-2.55002;24.4543,1.24998"/> + <dia:rectangle val="15.0693,3.89998;24.4543,7.69998"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="15.0693,-2.55002"/> + <dia:point val="15.0693,3.89998"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="9.3850000000000016"/> @@ -3335,16 +3335,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O23"> <dia:attribute name="obj_pos"> - <dia:point val="14.12,3.7"/> + <dia:point val="11.32,12.4"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="14.12,3.7;24.66,8.3"/> + <dia:rectangle val="11.32,12.4;21.86,17"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="14.12,3.7"/> + <dia:point val="11.32,12.4"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="10.539999999999999"/> @@ -3497,16 +3497,16 @@ </dia:object> <dia:object type="Database - Table" version="0" id="O24"> <dia:attribute name="obj_pos"> - <dia:point val="2.61,3.7125"/> + <dia:point val="-0.19,12.3625"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="2.61,3.7125;10.455,6.7125"/> + <dia:rectangle val="-0.19,12.3625;7.655,15.3625"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="elem_corner"> - <dia:point val="2.61,3.7125"/> + <dia:point val="-0.19,12.3625"/> </dia:attribute> <dia:attribute name="elem_width"> <dia:real val="7.8449999999999998"/> @@ -3613,19 +3613,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O25"> <dia:attribute name="obj_pos"> - <dia:point val="10.455,5.4125"/> + <dia:point val="7.655,14.0625"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="10.405,4.75;14.17,5.4625"/> + <dia:rectangle val="7.605,13.417;11.37,14.15"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="10.455,5.4125"/> - <dia:point val="11.95,5.4125"/> - <dia:point val="11.95,5.4"/> - <dia:point val="14.12,5.4"/> + <dia:point val="7.655,14.0625"/> + <dia:point val="9.8693,14.0625"/> + <dia:point val="9.8693,14.1"/> + <dia:point val="11.32,14.1"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -3673,19 +3673,19 @@ </dia:object> <dia:object type="Database - Reference" version="0" id="O26"> <dia:attribute name="obj_pos"> - <dia:point val="8.24583,-12.3834"/> + <dia:point val="8.19583,-5.7334"/> </dia:attribute> <dia:attribute name="obj_bb"> - <dia:rectangle val="5.87763,-13.0334;8.29583,-11.5334"/> + <dia:rectangle val="5.87763,-6.37895;8.24583,-4.8834"/> </dia:attribute> <dia:attribute name="meta"> <dia:composite type="dict"/> </dia:attribute> <dia:attribute name="orth_points"> - <dia:point val="8.24583,-12.3834"/> - <dia:point val="5.92763,-12.3834"/> - <dia:point val="5.92763,-11.5834"/> - <dia:point val="8.24583,-11.5834"/> + <dia:point val="8.19583,-5.7334"/> + <dia:point val="5.92763,-5.7334"/> + <dia:point val="5.92763,-4.9334"/> + <dia:point val="8.19583,-4.9334"/> </dia:attribute> <dia:attribute name="orth_orient"> <dia:enum val="0"/> @@ -3731,5 +3731,679 @@ <dia:connection handle="1" to="O11" connection="16"/> </dia:connections> </dia:object> + <dia:object type="Database - Table" version="0" id="O27"> + <dia:attribute name="obj_pos"> + <dia:point val="25.4168,-19.2167"/> + </dia:attribute> + <dia:attribute name="obj_bb"> + <dia:rectangle val="25.4168,-19.2167;34.4168,-12.2167"/> + </dia:attribute> + <dia:attribute name="meta"> + <dia:composite type="dict"/> + </dia:attribute> + <dia:attribute name="elem_corner"> + <dia:point val="25.4168,-19.2167"/> + </dia:attribute> + <dia:attribute name="elem_width"> + <dia:real val="9"/> + </dia:attribute> + <dia:attribute name="elem_height"> + <dia:real val="7"/> + </dia:attribute> + <dia:attribute name="name"> + dia:string#TaskFailures#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="visible_comment"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="underline_primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="tagging_comment"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="bold_primary_keys"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="attributes"> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#FailureId#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#JobId#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#StepNo#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT(2)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#TaskNo#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT(2)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#TaskLog#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(32)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#NewCount#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#OldCount#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + </dia:attribute> + <dia:attribute name="normal_font"> + <dia:font family="monospace" style="0" name="Courier"/> + </dia:attribute> + <dia:attribute name="name_font"> + <dia:font family="sans" style="80" name="Helvetica-Bold"/> + </dia:attribute> + <dia:attribute name="comment_font"> + <dia:font family="sans" style="8" name="Helvetica-Oblique"/> + </dia:attribute> + <dia:attribute name="normal_font_height"> + <dia:real val="0.80000000000000004"/> + </dia:attribute> + <dia:attribute name="name_font_height"> + <dia:real val="0.99999999999999989"/> + </dia:attribute> + <dia:attribute name="comment_font_height"> + <dia:real val="0.69999999999999996"/> + </dia:attribute> + <dia:attribute name="text_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="fill_colour"> + <dia:color val="#ffffffff"/> + </dia:attribute> + <dia:attribute name="line_width"> + <dia:real val="0.10000000000000001"/> + </dia:attribute> + </dia:object> + <dia:object type="Database - Table" version="0" id="O28"> + <dia:attribute name="obj_pos"> + <dia:point val="9.8001,-19.2167"/> + </dia:attribute> + <dia:attribute name="obj_bb"> + <dia:rectangle val="9.8001,-19.2167;21.1101,-9.0167"/> + </dia:attribute> + <dia:attribute name="meta"> + <dia:composite type="dict"/> + </dia:attribute> + <dia:attribute name="elem_corner"> + <dia:point val="9.8001,-19.2167"/> + </dia:attribute> + <dia:attribute name="elem_width"> + <dia:real val="11.309999999999999"/> + </dia:attribute> + <dia:attribute name="elem_height"> + <dia:real val="10.199999999999999"/> + </dia:attribute> + <dia:attribute name="name"> + dia:string#Failures#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="visible_comment"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="underline_primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="tagging_comment"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="bold_primary_keys"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="attributes"> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#Id#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#ErrorGroup#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(64)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#TestUnit#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(32)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#ConfigRegExp#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(64)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#FailureRegExp#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(256)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#Notes#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(128)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#LastNew#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#DATETIME#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#LastOld#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#DATETIME#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#BugId#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#INT#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#BugStatus#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(32)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + <dia:composite type="table_attribute"> + <dia:attribute name="name"> + dia:string#BugDescription#</dia:string> + </dia:attribute> + <dia:attribute name="type"> + dia:string#VARCHAR(128)#</dia:string> + </dia:attribute> + <dia:attribute name="comment"> + dia:string##</dia:string> + </dia:attribute> + <dia:attribute name="primary_key"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="nullable"> + <dia:boolean val="true"/> + </dia:attribute> + <dia:attribute name="unique"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="default_value"> + dia:string##</dia:string> + </dia:attribute> + </dia:composite> + </dia:attribute> + <dia:attribute name="normal_font"> + <dia:font family="monospace" style="0" name="Courier"/> + </dia:attribute> + <dia:attribute name="name_font"> + <dia:font family="sans" style="80" name="Helvetica-Bold"/> + </dia:attribute> + <dia:attribute name="comment_font"> + <dia:font family="sans" style="8" name="Helvetica-Oblique"/> + </dia:attribute> + <dia:attribute name="normal_font_height"> + <dia:real val="0.80000000000000004"/> + </dia:attribute> + <dia:attribute name="name_font_height"> + <dia:real val="0.99999999999999989"/> + </dia:attribute> + <dia:attribute name="comment_font_height"> + <dia:real val="0.69999999999999996"/> + </dia:attribute> + <dia:attribute name="text_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="fill_colour"> + <dia:color val="#ffffffff"/> + </dia:attribute> + <dia:attribute name="line_width"> + <dia:real val="0.10000000000000001"/> + </dia:attribute> + </dia:object> + <dia:object type="Database - Reference" version="0" id="O29"> + <dia:attribute name="obj_pos"> + <dia:point val="34.4168,-15.9167"/> + </dia:attribute> + <dia:attribute name="obj_bb"> + <dia:rectangle val="34.3668,-16.5622;37.6001,-5.6834"/> + </dia:attribute> + <dia:attribute name="meta"> + <dia:composite type="dict"/> + </dia:attribute> + <dia:attribute name="orth_points"> + <dia:point val="34.4168,-15.9167"/> + <dia:point val="37.5501,-15.9167"/> + <dia:point val="37.5501,-5.7334"/> + <dia:point val="36.0192,-5.7334"/> + </dia:attribute> + <dia:attribute name="orth_orient"> + <dia:enum val="0"/> + <dia:enum val="1"/> + <dia:enum val="0"/> + </dia:attribute> + <dia:attribute name="orth_autoroute"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="text_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_width"> + <dia:real val="0.10000000000000001"/> + </dia:attribute> + <dia:attribute name="line_style"> + <dia:enum val="0"/> + <dia:real val="1"/> + </dia:attribute> + <dia:attribute name="corner_radius"> + <dia:real val="0"/> + </dia:attribute> + <dia:attribute name="end_arrow"> + <dia:enum val="0"/> + </dia:attribute> + <dia:attribute name="start_point_desc"> + dia:string#0..n#</dia:string> + </dia:attribute> + <dia:attribute name="end_point_desc"> + dia:string#1#</dia:string> + </dia:attribute> + <dia:attribute name="normal_font"> + <dia:font family="monospace" style="0" name="Courier"/> + </dia:attribute> + <dia:attribute name="normal_font_height"> + <dia:real val="0.59999999999999998"/> + </dia:attribute> + dia:connections + <dia:connection handle="0" to="O27" connection="17"/> + <dia:connection handle="1" to="O12" connection="15"/> + </dia:connections> + </dia:object> + <dia:object type="Database - Reference" version="0" id="O30"> + <dia:attribute name="obj_pos"> + <dia:point val="21.1101,-17.5167"/> + </dia:attribute> + <dia:attribute name="obj_bb"> + <dia:rectangle val="21.0601,-18.1622;25.4668,-17.4667"/> + </dia:attribute> + <dia:attribute name="meta"> + <dia:composite type="dict"/> + </dia:attribute> + <dia:attribute name="orth_points"> + <dia:point val="21.1101,-17.5167"/> + <dia:point val="22.6693,-17.5167"/> + <dia:point val="22.6693,-17.5167"/> + <dia:point val="25.4168,-17.5167"/> + </dia:attribute> + <dia:attribute name="orth_orient"> + <dia:enum val="0"/> + <dia:enum val="1"/> + <dia:enum val="0"/> + </dia:attribute> + <dia:attribute name="orth_autoroute"> + <dia:boolean val="false"/> + </dia:attribute> + <dia:attribute name="text_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_colour"> + <dia:color val="#000000ff"/> + </dia:attribute> + <dia:attribute name="line_width"> + <dia:real val="0.10000000000000001"/> + </dia:attribute> + <dia:attribute name="line_style"> + <dia:enum val="0"/> + <dia:real val="1"/> + </dia:attribute> + <dia:attribute name="corner_radius"> + <dia:real val="0"/> + </dia:attribute> + <dia:attribute name="end_arrow"> + <dia:enum val="0"/> + </dia:attribute> + <dia:attribute name="start_point_desc"> + dia:string#1#</dia:string> + </dia:attribute> + <dia:attribute name="end_point_desc"> + dia:string#0..n#</dia:string> + </dia:attribute> + <dia:attribute name="normal_font"> + <dia:font family="monospace" style="0" name="Courier"/> + </dia:attribute> + <dia:attribute name="normal_font_height"> + <dia:real val="0.59999999999999998"/> + </dia:attribute> + dia:connections + <dia:connection handle="0" to="O28" connection="13"/> + <dia:connection handle="1" to="O27" connection="12"/> + </dia:connections> + </dia:object> </dia:layer> </dia:diagram> diff --git a/testbot/lib/WineTestBot/Failures.pm b/testbot/lib/WineTestBot/Failures.pm new file mode 100644 index 000000000..b37b9e046 --- /dev/null +++ b/testbot/lib/WineTestBot/Failures.pm @@ -0,0 +1,184 @@ +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# Copyright 2022 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + + +package WineTestBot::Failure; + +=head1 NAME + +WineTestBot::Failure - Describes a known Wine test failure + +=head1 DESCRIPTION + +Failure objects document known test failures and link them to the corresponding +bug describing the issue. They are mainly used to avoid reporting a test +failure as new when it has in fact happened before despite not being present +in the reference WineTest logs, either because it is just too rare, or because +its text is ever changing. + +Finally Failure objects are linked to the Task+Log they match by means of the +TaskFailure objects. + +=cut + +use WineTestBot::WineTestBotObjects; +our @ISA = qw(WineTestBot::WineTestBotItem); + + +sub Compare($$) +{ + my ($self, $B) = @_; + + # Sort deleted entries last + my %StatusOrders = ("deleted" => 1); + + my $Cmp = ($StatusOrders{$self->GetColValue("BugStatus")} || 0) <=> ($StatusOrders{$B->GetColValue("BugStatus")} || 0) || + $self->GetColValue("ErrorGroup") cmp $B->GetColValue("ErrorGroup") || + $self->GetColValue("TestUnit") cmp $B->GetColValue("TestUnit") || + $self->GetColValue("BugDescription") <=> $B->GetColValue("BugDescription"); + return $Cmp +} + +sub Validate($) +{ + my ($self) = @_; + + if ($self->ErrorGroup =~ /^ / or $self->ErrorGroup =~ / $/) + { + return ("ErrorGroup", "The error group '". $self->ErrorGroup ."' should not have leading or trailing spaces"); + } + if ($self->TestUnit !~ /^[_a-z0-9]*$/) + { + return ("TestUnit", "The test unit '". $self->TestUnit ."' contains invalid characters"); + } + foreach my $Pair (["ConfigRegExp", "configuration"], + ["FailureRegExp", "failure"]) + { + my ($Field, $Name) = @$Pair; + my $RegExp = $self->$Field; + my $ErrMessage = eval { + if ($RegExp and "" =~ /$RegExp/) + { + return "The $Name regular expression should not match empty strings"; + } + }; + $ErrMessage = "The $Name regular expression is invalid" if ($@); + return ($Field, $ErrMessage) if ($ErrMessage); + } + return $self->SUPER::Validate(); +} + + +package WineTestBot::Failures; + +=head1 NAME + +WineTestBot::Failures - A Failure collection + +=head1 DESCRIPTION + +This collection contains all known failures. + +=cut + +use Exporter 'import'; +use WineTestBot::WineTestBotObjects; +BEGIN +{ + our @ISA = qw(WineTestBot::WineTestBotCollection); + our @EXPORT = qw(CreateFailures); +} + +use ObjectModel::BasicPropertyDescriptor; +use ObjectModel::EnumPropertyDescriptor; +use ObjectModel::DetailrefPropertyDescriptor; +use WineTestBot::TaskFailures; + + +sub CreateItem($) +{ + my ($self) = @_; + + return WineTestBot::Failure->new($self); +} + +my @PropertyDescriptors = ( + # Rather than using a combination of the other fields, give each entry a + # unique id. This allows having multiple entries for a single Wine bug in + # case there are too many failures to match for the regular expression to + # fit in a single regular expression field. This also avoids using the + # regular expression as part of the primary key which would be troublesome + # as it may need to be adjusted in case it is buggy or if the test changes. + CreateBasicPropertyDescriptor("Id", "Id", 1, 1, "S", 10), + + # Identify the error group the failure can occur in. This is usually a + # test module but it may also be a group containing extra errors. Also some + # log files have a single nameless error group. + CreateBasicPropertyDescriptor("ErrorGroup", "Test module", !1, !1, "A", 64), + # For test modules, identify the test unit the failure can occur in. In the + # other cases this should be an empty string. + CreateBasicPropertyDescriptor("TestUnit", "Test unit", !1, !1, "A", 32), + + # A regular expression to match the configurations the failure can happen in. + # A configuration name is of the form 'VMNAME:REPORTNAME'. + CreateBasicPropertyDescriptor("ConfigRegExp", "Configurations RegExp", !1, !1, "A", 64), + + # A regular expression to match the troublesome failures. + CreateBasicPropertyDescriptor("FailureRegExp", "Failures RegExp", !1, 1, "A", 256), + + # Can be used for documentation when multiple entries are needed to match + # all the failures associated with a given bug. + # Can also be used to document when a regular expression has been modified, + # for instance to match changes in a test. + CreateBasicPropertyDescriptor("Notes", "Notes", !1, !1, "A", 128), + + # Record when an entry was last identified as a new or old failure, even + # after the corresponding tasks have expired (see the TaskFailures table). + CreateBasicPropertyDescriptor("LastNew", "Last new", !1, !1, "DT", 19), + CreateBasicPropertyDescriptor("LastOld", "Last old", !1, !1, "DT", 19), + + # Every entry must be associated with a Wine bug. + # Note: The 'deleted' bug status means this failure entry should be + # deleted as soon as it is not referenced anymore. + CreateBasicPropertyDescriptor("BugId", "WineHQ bug id", !1, 1, "N", 10), + CreateBasicPropertyDescriptor("BugStatus", "WineHQ bug status", !1, !1, "A", 32), + CreateBasicPropertyDescriptor("BugDescription", "WineHQ bug description", !1, !1, "A", 128), + + CreateDetailrefPropertyDescriptor("TaskFailures", "Tasks", &CreateTaskFailures), +); +SetDetailrefKeyPrefix("Failure", @PropertyDescriptors); + +=pod +=over 12 + +=item C<CreateFailures()> + +Creates a collection of Failure objects. + +=back +=cut + +sub CreateFailures(;$) +{ + my ($ScopeObject) = @_; + return WineTestBot::Failures->new("Failures", "Failures", "Failure", + @PropertyDescriptors, $ScopeObject); +} + +1; diff --git a/testbot/lib/WineTestBot/Jobs.pm b/testbot/lib/WineTestBot/Jobs.pm index 7f15a9c57..3da6ae7b0 100644 --- a/testbot/lib/WineTestBot/Jobs.pm +++ b/testbot/lib/WineTestBot/Jobs.pm @@ -89,6 +89,7 @@ use File::Path; use WineTestBot::Config; use WineTestBot::Branches; use WineTestBot::Engine::Notify; +use WineTestBot::TaskFailures;
sub _initialize($$) @@ -435,6 +436,11 @@ sub Restart($) my ($ErrProperty, $ErrMessage) = $self->Save(); # Save it all return "$ErrMessage ($ErrProperty)" if ($ErrMessage);
+ # Remove the corresponding TaskFailures if any + my $TaskFailures = CreateTaskFailures(); + $TaskFailures->AddFilter("JobId", [$self->Id]); + $TaskFailures->DeleteAll(); + return undef; }
diff --git a/testbot/lib/WineTestBot/TaskFailures.pm b/testbot/lib/WineTestBot/TaskFailures.pm new file mode 100644 index 000000000..bdbfd6e2e --- /dev/null +++ b/testbot/lib/WineTestBot/TaskFailures.pm @@ -0,0 +1,120 @@ +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# Copyright 2022 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + + +package WineTestBot::TaskFailure; + +=head1 NAME + +WineTestBot::TaskFailure - Ties a failure to its task+log matches + +=head1 DESCRIPTION + +A TaskFailure is created when a WineTestBot::Failure matches a line in a +task's log. It also records whether the matching failures occurred in previous +logs or not. This makes it possible to detect fixed failures. + +=cut + +use WineTestBot::WineTestBotObjects; +our @ISA = qw(WineTestBot::WineTestBotItem); + + +sub Compare($$) +{ + my ($self, $B) = @_; + + return $B->Task->Started <=> $self->Task->Started || # newest first by default + $self->TaskLog cmp $B->TaskLog; +} + + +package WineTestBot::TaskFailures; + +=head1 NAME + +WineTestBot::TaskFailures - A TaskFailure collection + +=head1 DESCRIPTION + +This collection cross references the known failures with the tasks and logs they +matched. + +=cut + +use Exporter 'import'; +use WineTestBot::WineTestBotObjects; +BEGIN +{ + our @ISA = qw(WineTestBot::WineTestBotCollection); + our @EXPORT = qw(CreateTaskFailures); +} + +use ObjectModel::BasicPropertyDescriptor; +use ObjectModel::ItemrefPropertyDescriptor; +use WineTestBot::Tasks; + + +sub CreateItem($) +{ + my ($self) = @_; + + return WineTestBot::TaskFailure->new($self); +} + +my @PropertyDescriptors = ( + # Identifies the task which has matching failures + CreateBasicPropertyDescriptor("JobId", "Job id", 1, 1, "N", 10), + CreateBasicPropertyDescriptor("StepNo", "Step no", 1, 1, "N", 2), + CreateBasicPropertyDescriptor("TaskNo", "Task", 1, 1, "N", 2), + CreateItemrefPropertyDescriptor("Task", "Task", 1, &WineTestBot::Tasks::CreateTasks, ["JobId", "StepNo", "TaskNo"]), + + # and more specifically in which of its logs + CreateBasicPropertyDescriptor("TaskLog", "Task log", 1, 1, "A", 32), + + # Also store a count of matching new and old failures + CreateBasicPropertyDescriptor("NewCount", "Count of matching new failures", !1, !1, "N", 10), + CreateBasicPropertyDescriptor("OldCount", "Count of matching old failures", !1, !1, "N", 10), +); +SetupItemrefColumns(@PropertyDescriptors); +my @FlatPropertyDescriptors = ( + CreateBasicPropertyDescriptor("FailureId", "Failure id", 1, 1, "N", 10), + @PropertyDescriptors +); + +=pod +=over 12 + +=item C<CreateTaskFailures()> + +Creates a collection of TaskFailure objects. + +=back +=cut + +sub CreateTaskFailures(;$$) +{ + my ($ScopeObject, $Failure) = @_; + return WineTestBot::TaskFailures->new("TaskFailures", + "TaskFailures", "TaskFailure", + $Failure ? @PropertyDescriptors : @FlatPropertyDescriptors, + $ScopeObject, $Failure); +} + +1; diff --git a/testbot/lib/WineTestBot/Tasks.pm b/testbot/lib/WineTestBot/Tasks.pm index 0360e4ebc..5b87c782b 100644 --- a/testbot/lib/WineTestBot/Tasks.pm +++ b/testbot/lib/WineTestBot/Tasks.pm @@ -341,7 +341,9 @@ BEGIN use ObjectModel::BasicPropertyDescriptor; use ObjectModel::EnumPropertyDescriptor; use ObjectModel::ItemrefPropertyDescriptor; +use ObjectModel::DetailrefPropertyDescriptor; use WineTestBot::VMs; +use WineTestBot::TaskFailures;
sub CreateItem($) @@ -362,8 +364,10 @@ my @PropertyDescriptors = ( CreateBasicPropertyDescriptor("Started", "Started", !1, !1, "DT", 19), CreateBasicPropertyDescriptor("Ended", "Ended", !1, !1, "DT", 19), CreateBasicPropertyDescriptor("TestFailures", "Failures", !1, !1, "N", 6), + CreateDetailrefPropertyDescriptor("Failures", "Known failures", &CreateTaskFailures), ); SetupItemrefColumns(@PropertyDescriptors); +SetDetailrefKeyPrefix("Task", @PropertyDescriptors); my @FlatPropertyDescriptors = ( CreateBasicPropertyDescriptor("JobId", "Job id", 1, 1, "N", 10), CreateBasicPropertyDescriptor("StepNo", "Step no", 1, 1, "N", 2),
The pages allow everyone to see the known failures. But only TestBot administrators can create and modify them.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=48912 Signed-off-by: Francois Gouget fgouget@codeweavers.com --- The plan is for the failure details page to later show all tasks where the failure was found. That would allow developers to compare the logs for diagnosis.
Adding a known failure is a sensitive operation as it causes that failure to essentially be ignored. This means if the regular expression is too broad whole classes of failures could end up being ignored. So for now only the TestBot administrators can add them. Another option would be to allow anyone with a login to add them, or allow adding them but requiring a validation step by a TestBot administrator (a bit like when new users are added). But I think that can wait until we see how this goes. --- testbot/lib/WineTestBot/CGI/PageBase.pm | 1 + testbot/lib/WineTestBot/Config.pm | 6 +- testbot/web/FailureDetails.pl | 119 ++++++++++++++++++ testbot/web/FailuresList.pl | 160 ++++++++++++++++++++++++ testbot/web/WineTestBot.css | 3 + 5 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 testbot/web/FailureDetails.pl create mode 100644 testbot/web/FailuresList.pl
diff --git a/testbot/lib/WineTestBot/CGI/PageBase.pm b/testbot/lib/WineTestBot/CGI/PageBase.pm index d6f33e2ca..0486bbefc 100644 --- a/testbot/lib/WineTestBot/CGI/PageBase.pm +++ b/testbot/lib/WineTestBot/CGI/PageBase.pm @@ -521,6 +521,7 @@ EOF print " <li><p><a href='/PatchesList.pl'>", ucfirst($PatchesMailingList), "</a></p></li>\n"; } + print " <li><p><a href='/FailuresList.pl'>Failures</a></p></li>\n";
my $Session = $self->GetCurrentSession(); if ($self->SessionActive()) diff --git a/testbot/lib/WineTestBot/Config.pm b/testbot/lib/WineTestBot/Config.pm index 089d792c5..6f57995e8 100644 --- a/testbot/lib/WineTestBot/Config.pm +++ b/testbot/lib/WineTestBot/Config.pm @@ -31,7 +31,7 @@ use vars qw (@ISA @EXPORT @EXPORT_OK $UseSSL $LogDir $DataDir $BinDir $MaxVMsWhenIdle $WaitForBoot $SleepAfterBoot $SleepAfterRevert $VMToolTimeout $WaitForShutdown $MaxVMErrors $MaxTaskTries $AdminEMail $RobotEMail - $WinePatchToOverride $WinePatchCc + $WinePatchToOverride $WinePatchCc $WineBugUrl $ReconfigBuildTimeout $ExeBuildTimeout $ExeModuleTimeout $WineBuildTimeout $WineModuleTimeout $TimeoutMargin $SuiteTimeout $SingleTimeout $SingleAvgTime $MaxUnitSize @@ -48,7 +48,7 @@ require Exporter; $MaxRunningVMs $MaxVMsWhenIdle $WaitForBoot $SleepAfterBoot $SleepAfterRevert $VMToolTimeout $WaitForShutdown $MaxVMErrors $MaxTaskTries $AdminEMail - $RobotEMail $WinePatchToOverride $WinePatchCc $SuiteTimeout + $RobotEMail $WinePatchToOverride $WinePatchCc $WineBugUrl $ReconfigBuildTimeout $ExeBuildTimeout $ExeModuleTimeout $WineBuildTimeout $WineModuleTimeout $TimeoutMargin $SuiteTimeout $SingleTimeout $SingleAvgTime $MaxUnitSize @@ -85,6 +85,8 @@ sub PrependPaths(@) "wine" => "git://source.winehq.org/git/wine.git", );
+$WineBugUrl = "https://bugs.winehq.org/show_bug.cgi?id="; + # See the ScheduleOnHost() documentation in lib/WineTestBot/Jobs.pm $MaxRevertingVMs = 1; $MaxRevertsWhileRunningVMs = 0; diff --git a/testbot/web/FailureDetails.pl b/testbot/web/FailureDetails.pl new file mode 100644 index 000000000..7cab1d2a6 --- /dev/null +++ b/testbot/web/FailureDetails.pl @@ -0,0 +1,119 @@ +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# Failure details page +# +# Copyright 2022 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + +package FailureDetailsPage; + +use ObjectModel::CGI::ItemPage; +our @ISA = qw(ObjectModel::CGI::ItemPage); + +use WineTestBot::Config; +use WineTestBot::Failures; + + +sub _initialize($$$) +{ + my ($self, $Request, $RequiredRole) = @_; + + $self->SUPER::_initialize($Request, $RequiredRole, CreateFailures()); + + my $Session = $self->GetCurrentSession(); + if (!$Session or !$Session->User->HasRole("admin")) + { + # Only admin users can create and modify failures + exit($self->RedirectToParent()) if ($self->{Item}->GetIsNew()); + $self->SetReadWrite(0); + } +} + +sub DisplayProperty($$) +{ + my ($self, $PropertyDescriptor) = @_; + + my $PropertyName = $PropertyDescriptor->GetName(); + return # Already shown in the title + $PropertyName eq "Id" ? "" : + # Showing these makes no sense for new items since they are read-only + $PropertyName =~ /^(?:LastNew|LastOld|BugStatus|BugDescription)$/ ? + ($self->{Item}->GetIsNew() ? "" : "ro") : + $self->SUPER::DisplayProperty($PropertyDescriptor); +} + +sub GenerateValueView($$$$) +{ + my ($self, $PropertyDescriptor, $Value, $Format) = @_; + + if ($PropertyDescriptor->GetName() =~ /^Bug(?:Id|Status|Description)$/) + { + print "<a href='$WineBugUrl", $self->{Item}->BugId, "' target='_blank'>", + $self->escapeHTML($Value || "Not fetched yet..."), "</a>"; + } + else + { + $self->SUPER::GenerateValueView($PropertyDescriptor, $Value, $Format); + } +} + +sub GenerateFooter($) +{ + my ($self) = @_; + print "<p></p><div class='CollectionBlock'><table>\n"; + print "<thead><tr><th class='Record'>Legend</th></tr></thead>\n"; + print "<tbody><tr><td>\n"; + + print "<p>The <b>Test module</b> is in fact the name of the error group the failure may occur in. Most of the time this is a test module but it may also be the name of the group containing the extra errors.<br>\n"; + print "The <b>Test unit</b> is the name of the test unit the failure may occur in. For extra errors this should be an empty string.<br>\n"; + print "The <b>Configurations RegExp</b> defines which test configurations are known to exhibit this failure. It should match the configuration names which are of the form <i>VM:logfile</i> where <i>VM</i> is the name of the virtual machine (which for Windows VMs includes the test configuration); and <i>logfile</i> is a task log file, the most important of which are the test reports: <i>exe32</i> or <i>exe64</i> for Windows, and <i>win32</i>, <i>wow32</i> or <i>wow64</i> for Wine. On Wine the log filename may contain test configuration information, typically the locale, in an underscore separated suffix.<br>\n"; + print "The <b>Failures RegExp</b> should match the failure lines. Matching the test unit name is not necessary. Beware of the presence of wildcards in the error message such as: *+|()[]{}<br>\n"; + + print "</td></tr></tbody>\n"; + print "</table></div>\n"; + $self->SUPER::GenerateFooter(); +} + +sub OnAction($$) +{ + my ($self, $Action) = @_; + + if ($Action eq "Save") + { + if (!$self->{RW}) + { + $self->{ErrMessage} = "Only admin users can create or modify failures"; + return 0; + } + if (($self->{Item}->BugId || "") ne $self->GetParam("BugId")) + { + $self->{Item}->BugStatus(""); + $self->{Item}->BugDescription(""); + } + return undef if (!$self->Save()); + # FIXME Notify the TestBot Engine so it updates the bug information + exit($self->RedirectToParent()); + } + return $self->SUPER::OnAction($Action); +} + + +package main; + +my $Request = shift; +my $Page = FailureDetailsPage->new($Request, ""); +$Page->GeneratePage(); diff --git a/testbot/web/FailuresList.pl b/testbot/web/FailuresList.pl new file mode 100644 index 000000000..f2608dc45 --- /dev/null +++ b/testbot/web/FailuresList.pl @@ -0,0 +1,160 @@ +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# Known failures index page +# +# Copyright 2022 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + +package FailuresBlock; + +use ObjectModel::CGI::CollectionBlock; +our @ISA = qw(ObjectModel::CGI::CollectionBlock); + +use WineTestBot::Config; +use WineTestBot::Failures; + + +sub _initialize($$$) +{ + my ($self, $Collection, $EnclosingPage) = @_; + + $self->SUPER::_initialize($Collection, $EnclosingPage); + + my $Session = $EnclosingPage->GetCurrentSession(); + if (!$Session or !$Session->User->HasRole("admin")) + { + $self->SetReadWrite(0); + # and hide the deleted entries + $self->{Collection}->AddFilter("BugStatus", ["deleted"], "<>"); + } +} + +sub Create($$) +{ + my ($Collection, $EnclosingPage) = @_; + + return FailuresBlock->new($Collection, $EnclosingPage); +} + +sub DisplayProperty($$) +{ + my ($self, $PropertyDescriptor) = @_; + + my $PropertyName = $PropertyDescriptor->GetName(); + return $PropertyName =~ /^(?:Notes|ConfigRegExp|FailureRegExp|BugId)$/ ? "" : + $self->SUPER::DisplayProperty($PropertyDescriptor); +} + +sub GenerateHeaderView($$$) +{ + my ($self, $Row, $Col) = @_; + + my $PropertyName = $Col->{Descriptor}->GetName(); + if ($PropertyName eq "ErrorGroup") + { + print "<a class='title' title='or Error group'>Test module</a>"; + } + else + { + $self->SUPER::GenerateHeaderView($Row, $Col); + } +} + +sub GenerateDataView($$$) +{ + my ($self, $Row, $Col) = @_; + + my $PropertyName = $Col->{Descriptor}->GetName(); + if ($PropertyName =~ /^Test(?:Module|Unit)$/) + { + print "<a href='", $self->escapeHTML($self->GetDetailsLink($Row)), "'>", + $self->escapeHTML($Row->{Item}->$PropertyName), "</a>"; + } + elsif ($PropertyName eq "BugStatus") + { + my $Status = $Row->{Item}->BugStatus || "Not fetched yet..."; + my $Class = ($Status eq "Does not exist") ? "-missing" : + ($Status =~ /^(?:CLOSED|RESOLVED)/) ? "-closed" : ""; + print "<span class='bug$Class'>", $self->escapeHTML($Status), "</span>"; + } + elsif ($PropertyName eq "BugId") + { + print "<a href='$WineBugUrl", $Row->{Item}->BugId, "' target='_blank'>", + $self->escapeHTML($Row->{Item}->BugId), "</a>"; + } + elsif ($PropertyName eq "BugDescription") + { + my $Value = $Row->{Item}->BugDescription || "Not fetched yet..."; + my $Tooltip =""; + if ($Row->{Item}->Notes) + { + $Tooltip = " title='". $self->escapeHTML($Row->{Item}->Notes) ."'"; + } + print "<a href='", $self->escapeHTML($self->GetDetailsLink($Row)), + "'$Tooltip>", $self->escapeHTML($Value), "</a>"; + } + else + { + $self->SUPER::GenerateDataView($Row, $Col); + } +} + +sub GetItemActions($) +{ + my ($self) = @_; + + return ["Delete", "Restore"]; +} + +sub OnItemAction($$$) +{ + my ($self, $Failure, $Action) = @_; + + if ($self->{RW}) + { + return 1 if ($Action eq "Restore" and $Failure->BugStatus ne "deleted"); + + my $NewStatus = $Action eq "Delete" ? "deleted" : + $Action eq "Restore" ? "unknown" : + undef; + if ($NewStatus) + { + $Failure->BugStatus($NewStatus); + my ($_ErrProperty, $ErrMessage) = $Failure->Save(); + if (defined $ErrMessage) + { + # Setting ErrField is only useful on form pages + $self->{EnclosingPage}->SetError(undef, $ErrMessage); + return 0; + } + # FIXME Notify the TestBot Engine so it updates the bug information + return 1; + } + } + + return $self->SUPER::OnItemAction($Failure, $Action); +} + + +package main; + +use ObjectModel::CGI::CollectionPage; +use WineTestBot::Failures; + +my $Request = shift; +my $Page = ObjectModel::CGI::CollectionPage->new($Request, "", CreateFailures(), &FailuresBlock::Create); +$Page->GeneratePage(); diff --git a/testbot/web/WineTestBot.css b/testbot/web/WineTestBot.css index fd6fca359..5e0c39ff4 100644 --- a/testbot/web/WineTestBot.css +++ b/testbot/web/WineTestBot.css @@ -407,6 +407,9 @@ pre .log-new { color: #e56e00; font-weight: bold; } .log-fullnew { color: red; font-weight: bold; }
+.bug-missing { color: red; font-weight: bold; } +.bug-closed { color: red; } + a.title { color:inherit; text-decoration: none; } a.tipdatetime { color:inherit; text-decoration: none; } a.timetipdate { color:inherit; text-decoration: none; }
This augments the .errors files with information mapping errors to known failures.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=48912 Signed-off-by: Francois Gouget fgouget@codeweavers.com --- It would probably be more space-efficient to have one line per known failure with a list of all the matching error indices. But that does not match the way we use the information and this should not change all that much in terms of space usage. So I went with the simpler error index to known failures mapping. --- testbot/lib/WineTestBot/LogUtils.pm | 180 +++++++++++++++++++++++++++- 1 file changed, 178 insertions(+), 2 deletions(-)
diff --git a/testbot/lib/WineTestBot/LogUtils.pm b/testbot/lib/WineTestBot/LogUtils.pm index 2e3de83b8..b8aaaa43c 100644 --- a/testbot/lib/WineTestBot/LogUtils.pm +++ b/testbot/lib/WineTestBot/LogUtils.pm @@ -36,7 +36,10 @@ our @EXPORT = qw(GetLogFileNames GetLogLabel use Algorithm::Diff; use File::Basename;
+use ObjectModel::Collection; # For CombineKey() use WineTestBot::Config; # For $MaxUnitSize +use WineTestBot::Failures; +use WineTestBot::Tasks; # FIXME Hack for SaveLogFailures() use WineTestBot::Utils; # For LocaleName()
@@ -187,6 +190,12 @@ sub _AddLogError($$$;$$) Returns a hashtable containing a summary of the task log: =over
+=item LogName +The log file basename. + +=item LogPath +The full log file path. + =item Type 'tests' if the task ran Wine tests and 'build' otherwise.
@@ -913,6 +922,10 @@ copy of that line (with CR/LF converted to a simple LF). The format for new error lines is identical to that for old errors but with a different type.
+=item f <errindex> <failureid1>... +Failure lines contain the index of the error in the error group and a list of +the known failures they match (normally at most one). + =back =back =cut @@ -981,6 +994,10 @@ sub LoadLogErrorsFromFh($$) { _AddLogError($LogInfo, $LogInfo->{CurGroup}, $Value, $Property, "new"); } + elsif ($Type eq "f") + { + $LogInfo->{CurGroup}->{Failures}->{$Property} = [ split / /, $Value ]; + } else { $LogInfo->{BadLog} = "$LogInfo->{LineNo}: Found an unknown line type ($Type)"; @@ -1058,8 +1075,12 @@ sub _WriteLogErrorsToFh($$) print $Fh "g $Group->{LineNo} $GroupName\n"; foreach my $Index (0..$#{$Group->{Errors}}) { + my $LineNo = $Group->{LineNos}->[$Index]; my $IsNew = $Group->{IsNew}->[$Index] ? "n" : "o"; - print $Fh "$IsNew $Group->{LineNos}->[$Index] $Group->{Errors}->[$Index]\n"; + print $Fh "$IsNew $LineNo $Group->{Errors}->[$Index]\n"; + + my $Failures = $Group->{Failures}->{$Index}; + print $Fh "f $Index @$Failures\n" if ($Failures); } } } @@ -1299,6 +1320,158 @@ sub TagNewErrors($$) } }
+=pod +=over 12 + +=item C<MatchLogFailures()> + +Checks the errors against known failures. + +The $LogInfo structure is augmented with the following fields: +=over + +=item ErrGroups +=over + +=item Failures +A hashtable mapping error indices to the list of matching known +failure ids. + +=back +=back + +Returns a hashtable containing a summary of the log failures: +=over + +=item LogName +The log file basename. + +=item Collection +A collection containing the relevant Failure objects. + +=item Failures +A hashtable indexed by the failure ids. Each entry contains: + +=over + +=item Failure +The failure object. + +=item NewCount +A count errors matched by this known failure that were tagged as new. + +=item OldCount +A count errors matched by this known failure that were tagged as old. + +=back + +=back +=back +=cut + +sub MatchLogFailures($$) +{ + my ($LogInfo, $Task) = @_; + + my $LogFailures = { + Task => $Task, + LogName => $LogInfo->{LogName} + }; + return $LogFailures if (!$LogInfo->{ErrCount}); + + my %FailureTree; + my $ConfigName = $Task->VMName .":$LogInfo->{LogName}"; + + $LogFailures->{Collection} = CreateFailures(); + foreach my $Failure (@{$LogFailures->{Collection}->GetItems()}) + { + # Ignore failures that don't apply to this configuration + my $ConfigRegExp = $Failure->ConfigRegExp; + my $Match = eval { $ConfigRegExp and $ConfigName =~ /$ConfigRegExp/ }; + next if (!$Match); + + my $UnitFailures = $FailureTree{$Failure->ErrorGroup}->{$Failure->TestUnit} ||= []; + push @$UnitFailures, $Failure; + } + + foreach my $GroupName (@{$LogInfo->{ErrGroupNames}}) + { + next if (!$FailureTree{$GroupName}); + + my $Group = $LogInfo->{ErrGroups}->{$GroupName}; + foreach my $ErrIndex (0..$#{$Group->{Errors}}) + { + my $Line = $Group->{Errors}->[$ErrIndex]; + my $TestUnit = $Line =~ /^([_a-z0-9]+).c:\d+:/ ? $1 : ""; + my $UnitFailures = $FailureTree{$GroupName}->{$TestUnit}; + next if (!$UnitFailures); + + foreach my $UnitFailure (@$UnitFailures) + { + my $RegExp = $UnitFailure->FailureRegExp; + my $Match = eval { $RegExp and $Line =~ /$RegExp/ }; + next if (!$Match); + + my $LineFailures = $Group->{Failures}->{$ErrIndex} ||= []; + push @$LineFailures, $UnitFailure->Id; + + my $LogFailure = $LogFailures->{Failures}->{$UnitFailure->Id}; + if (!$LogFailure) + { + $LogFailure = $LogFailures->{Failures}->{$UnitFailure->Id} = + { Failure => $UnitFailure }; + } + my $Count = $Group->{IsNew}->[$ErrIndex] ? "NewCount" : "OldCount"; + $LogFailure->{$Count}++; + } + } + } + + return $LogFailures; +} + +=pod +=over 12 + +=item C<SaveLogFailures()> + +Updates the TaskFailures objects for the task and log specified by the +$LogFailures structure. + +Note that this implies deleting any preexisting TaskFailure to avoid leaving +obsolete data. + +=back +=cut + +sub SaveLogFailures($) +{ + my ($LogFailures) = @_; + + # FIXME $Task->Failures->AddFilter() cannot be undone and impacts every + # future use of $Task->Failures. So add the filter on a throw away + # clone to not end up with a nonsensical filter. + my $TaskFailures = $LogFailures->{Task}->Failures->Clone(); + $TaskFailures->AddFilter("TaskLog", [$LogFailures->{LogName}]); + my $ErrMessage = $TaskFailures->DeleteAll(); + return $ErrMessage if (defined $ErrMessage); + return undef if (!$LogFailures->{Failures}); + + foreach my $LogFailure (values %{$LogFailures->{Failures}}) + { + my $TaskFailure = $LogFailure->{Failure}->TaskFailures->Add(); + my $OldKey = $TaskFailure->GetKey(); + $TaskFailure->Task($LogFailures->{Task}); + $TaskFailure->TaskLog($LogFailures->{LogName}); + $TaskFailure->KeyChanged($OldKey, $TaskFailure->GetKey()); + $TaskFailure->NewCount($LogFailure->{NewCount}); + $TaskFailure->OldCount($LogFailure->{OldCount}); + } + + (my $_ErrKey, my $_ErrProperty, $ErrMessage) = $LogFailures->{Collection}->Save(); + return $ErrMessage; +} +
# # Log errors caching [Part 2] @@ -1322,8 +1495,11 @@ sub CreateLogErrorsCache($;$) # Don't mark the errors as new if there is no reference WineTest report # as this would cause false positives. } + my $LogFailures = MatchLogFailures($LogInfo, $Task) if ($Task);
- return _SaveLogErrors($LogInfo); + my $ErrMessage = _SaveLogErrors($LogInfo); + $ErrMessage ||= SaveLogFailures($LogFailures) if ($Task); + return $ErrMessage; }
When a log contains a known failure, link it to the corresponding failure details page(s).
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=48912 Signed-off-by: Francois Gouget fgouget@codeweavers.com --- testbot/web/JobDetails.pl | 54 +++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-)
diff --git a/testbot/web/JobDetails.pl b/testbot/web/JobDetails.pl index 7860a4c9d..efdd4d9b0 100644 --- a/testbot/web/JobDetails.pl +++ b/testbot/web/JobDetails.pl @@ -237,6 +237,7 @@ our @ISA = qw(ObjectModel::CGI::CollectionPage); use URI::Escape;
use WineTestBot::Config; +use WineTestBot::Failures; use WineTestBot::Jobs; use WineTestBot::Log; # For Elapsed() use WineTestBot::LogUtils; @@ -260,6 +261,7 @@ sub _initialize($$$) { $self->SetRefreshInterval(30); } + $self->{Failures} = CreateFailures(); }
sub GetJob($) @@ -378,6 +380,30 @@ sub GenerateMoreInfoLink($$$$;$$) print "<div class='TaskMoreInfoLink'>$Html</div>\n"; }
+sub GetFailureLink($$;$) +{ + my ($self, $Id, $Str) = @_; + + my $Failure = $self->{Failures}->GetItem($Id); + $Str ||= $Failure ? $Failure->BugId : "F$Id"; + my $Desc = $Failure->BugDescription if ($Failure); + $Desc = ": ". $self->escapeHTML($Desc) if ($Desc); + return "<a href='FailureDetails.pl?Key=$Id' title='Failure $Id$Desc'>$Str</a>"; +} + +sub GetFailureLinks($$) +{ + my ($self, $FailureIds) = @_; + + return join(" ", map { $self->GetFailureLink($_) } + sort { + my $FA = $self->{Failures}->GetItem($a); + my $FB = $self->{Failures}->GetItem($b); + return ($FA ? $FA->BugId : 0) <=> ($FB ? $FB->BugId : 0) + || $a <=> $b; + } @$FailureIds); +} + sub GenerateFullLog($$$$) { my ($self, $Dir, $LogName, $HideLog) = @_; @@ -400,14 +426,18 @@ sub GenerateFullLog($$$$) print "<pre class='log-note'>Some WineTest results could not be used to detect new errors: $LogInfo->{BadRef}</pre>\n"; }
- my %ErrCategory; + my (%ErrCategory, %ErrFailures); foreach my $GroupName (@{$LogInfo->{ErrGroupNames}}) { my $Group = $LogInfo->{ErrGroups}->{$GroupName}; for my $ErrIndex (0..$#{$Group->{Errors}}) { my $LineNo = $Group->{LineNos}->[$ErrIndex]; - $ErrCategory{$LineNo} = $Group->{IsNew}->[$ErrIndex] ? "fullnew" : "error"; + if ($LineNo) + { + $ErrCategory{$LineNo} = $Group->{IsNew}->[$ErrIndex] ? "fullnew" : "error"; + $ErrFailures{$LineNo} = $Group->{Failures}->{$ErrIndex}; + } } }
@@ -435,6 +465,11 @@ sub GenerateFullLog($$$$) { $Html =~ s~^(.*\S)\s*\r?$~<span class='log-$Category'>$1</span>~; } + if ($ErrFailures{$LineNo}) + { + print "[", $self->GetFailureLinks($ErrFailures{$LineNo}), "] "; + $Html = $self->GetFailureLink($ErrFailures{$LineNo}->[0], $Html); + } print "$Html\n"; } close($LogFile); @@ -464,6 +499,11 @@ sub GenerateFullLog($$$$) my $Line = $Group->{Errors}->[$ErrIndex]; my $Category = $Group->{IsNew}->[$ErrIndex] ? "new" : "error"; my $Html = $self->escapeHTML($Line); + if ($Group->{Failures}->{$ErrIndex}) + { + print "[", $self->GetFailureLinks($Group->{Failures}->{$ErrIndex}), "] "; + $Html = $self->GetFailureLink($Group->{Failures}->{$ErrIndex}->[0], $Html); + } print "<span class='log-$Category'>$Html</span>\n"; } } @@ -629,14 +669,18 @@ EOF my $Group = $LogInfo->{ErrGroups}->{$GroupName}; foreach my $Line (@{$Group->{Errors}}) { + my $Html = $self->escapeHTML($Line); if ($Group->{IsNew}->[$ErrIndex]) { - print "<span class='log-new'>", $self->escapeHTML($Line), "</span>\n"; + $Html = "<span class='log-new'>$Html</span>"; } - else + my $FailureIds = $Group->{Failures}->{$ErrIndex}; + if ($FailureIds) { - print $self->escapeHTML($Line), "\n"; + print "[", $self->GetFailureLinks($FailureIds), "] "; + $Html = $self->GetFailureLink($FailureIds->[0], $Html); } + print "$Html\n"; $ErrIndex++; } print "</code></pre>\n";
Don't count known failures as new failures, in particular when updating the patches site. Add links to the WineHQ bugs for known failures found in the logs. And identify the bug each known failure line corresponds to.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=48912 Signed-off-by: Francois Gouget fgouget@codeweavers.com --- testbot/bin/WineSendLog.pl | 78 ++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 24 deletions(-)
diff --git a/testbot/bin/WineSendLog.pl b/testbot/bin/WineSendLog.pl index 3897ac034..a2f2369b6 100755 --- a/testbot/bin/WineSendLog.pl +++ b/testbot/bin/WineSendLog.pl @@ -44,6 +44,7 @@ $Name0 =~ s+^.*/++; use Algorithm::Diff;
use WineTestBot::Config; +use WineTestBot::Failures; use WineTestBot::Jobs; use WineTestBot::Log; use WineTestBot::LogUtils; @@ -177,10 +178,11 @@ sub SendLog($) # Collect and count the new and old failures, etc. #
- my (@New, $JobInfo); - my ($NewCount, $OldCount, $BotCount, $NotRun) = (0, 0, 0, 0); + my ($JobInfo, @New, %BugDescriptions); + my ($NewCount, $FailCount, $OldCount, $BotCount, $NotRun) = (0, 0, 0, 0, 0); my $FileType = "test executable";
+ my $Failures = CreateFailures($Job); my $StepsTasks = CreateStepsTasks(undef, $Job); my $SortedStepsTasks = $StepsTasks->GetSortedItems(); foreach my $StepTask (@$SortedStepsTasks) @@ -193,12 +195,13 @@ sub SendLog($)
my $TaskDir = $StepTask->GetTaskDir(); my $LogNames = GetLogFileNames($TaskDir); - $JobInfo->{$StepTask->Id}->{LogNames} = $LogNames; - $JobInfo->{$StepTask->Id}->{NewCount} = 0; - $JobInfo->{$StepTask->Id}->{OldCount} = 0; + my $TaskInfo = $JobInfo->{$StepTask->Id} = {}; + $TaskInfo->{LogNames} = $LogNames; + $TaskInfo->{NewCount} = 0; + $TaskInfo->{OldCount} = 0; if ($StepTask->Status eq "boterror") { - $JobInfo->{$StepTask->Id}->{BotCount} = 1; + $TaskInfo->{BotCount} = 1; $BotCount++; } $FileType = "patch" if ($StepTask->FileType eq "patch"); @@ -207,7 +210,7 @@ sub SendLog($) { my $LogInfo = LoadLogErrors("$TaskDir/$LogName"); next if (!defined $LogInfo->{BadLog} and !$LogInfo->{ErrCount}); - $JobInfo->{$StepTask->Id}->{$LogName} = $LogInfo; + $TaskInfo->{$LogName} = $LogInfo;
my $HasLogHeader; foreach my $GroupName (@{$LogInfo->{ErrGroupNames}}) @@ -216,7 +219,21 @@ sub SendLog($) my $Group = $LogInfo->{ErrGroups}->{$GroupName}; foreach my $ErrIndex (0..$#{$Group->{Errors}}) { - if ($Group->{IsNew}->[$ErrIndex]) + my $LineNo = $Group->{LineNos}->[$ErrIndex]; + my $LineFailureIds = $Group->{Failures}->{$ErrIndex}; + if ($LineFailureIds) + { + foreach my $FailureId (@$LineFailureIds) + { + my $Failure = $Failures->GetItem($FailureId); + next if (!$Failure); + $BugDescriptions{$Failure->BugId} = $Failure->BugDescription; + # Add bug information to $LogInfo + push @{$LogInfo->{Bugs}->{$LineNo}}, $Failure->BugId; + } + $TaskInfo->{OldCount}++; + } + elsif ($Group->{IsNew}->[$ErrIndex]) { if (!$HasLogHeader) { @@ -229,16 +246,16 @@ sub SendLog($) $HasGroupHeader = 1; } push @New, "$Group->{Errors}->[$ErrIndex]\n"; - $JobInfo->{$StepTask->Id}->{NewCount}++; + $TaskInfo->{NewCount}++; } else { - $JobInfo->{$StepTask->Id}->{OldCount}++; + $TaskInfo->{OldCount}++; } } } - $NewCount += $JobInfo->{$StepTask->Id}->{NewCount}; - $OldCount += $JobInfo->{$StepTask->Id}->{OldCount}; + $NewCount += $TaskInfo->{NewCount}; + $OldCount += $TaskInfo->{OldCount}; } }
@@ -281,10 +298,9 @@ sub SendLog($) { print $Sendmail <<"EOF"; It looks like your $FileType introduces some new failures. Please -investigate and fix them if they are indeed new. Note that rare -failures and failures with always changing text (e.g. because of memory -addresses) can cause false positives. If this is what happened, then -fixing those would really help. +investigate and fix them if they are indeed new. If they are not new, +fixing them anyway would help a lot. Otherwise please ask for the known +failures list to be updated.
EOF } @@ -309,10 +325,21 @@ EOF if ($OldCount) { print $Sendmail <<"EOF"; -Some preexisting failures (not caused by your $FileType) happened. +There are some preexisting failures (not caused by your $FileType). If you know how to fix them that would be helpful. - EOF + if (%BugDescriptions) + { + print $Sendmail <<"EOF"; +In particular some failures are tracked in the bug(s) below: +EOF + foreach my $BugId (sort { $a <=> $b } keys %BugDescriptions) + { + print $Sendmail "* $BugId - $BugDescriptions{$BugId}\n"; + print $Sendmail " $WineBugUrl$BugId\n"; + } + print $Sendmail "\n"; + } }
print $Sendmail " Failures\n"; @@ -360,7 +387,7 @@ EOF my $TaskDir = $StepTask->GetTaskDir(); foreach my $LogName (@{$TaskInfo->{LogNames}}) { - my $LogInfo = LoadLogErrors("$TaskDir/$LogName"); + my $LogInfo = $TaskInfo->{$LogName}; next if (!defined $LogInfo->{BadLog} and !$LogInfo->{ErrCount});
print $Sendmail "\n=== ", GetTitle($StepTask, $LogName), " ===\n"; @@ -372,7 +399,11 @@ EOF my $Group = $LogInfo->{ErrGroups}->{$GroupName}; foreach my $ErrIndex (0..$#{$Group->{Errors}}) { - my $Prefix = $Group->{IsNew}->[$ErrIndex] ? "new" : "old"; + my $LineNo = $Group->{LineNos}->[$ErrIndex]; + my $Bugs = $LogInfo->{Bugs}->{$LineNo}; + my $Prefix = $Bugs ? join(" ", sort { $a <=> $b } @$Bugs) : + $Group->{IsNew}->[$ErrIndex] ? "new" : + "old"; print $Sendmail "[$Prefix] $Group->{Errors}->[$ErrIndex]\n"; } } @@ -433,10 +464,9 @@ EOF { print $Sendmail <<"EOF"; It looks like your $FileType introduced the new failures shown below. -Please investigate and fix them if they are indeed new. Note that rare -failures and failures with always changing text (e.g. because of memory -addresses) can cause false positives. If this is what happened, then -fixing those would really help. +Please investigate and fix them before resubmitting your $FileType. +If they are not new, fixing them anyway would help a lot. Otherwise +please ask for the known failures list to be updated.
EOF }
This queries Bugzilla to update the failure bug status and description fields.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=48912 Signed-off-by: Francois Gouget fgouget@codeweavers.com --- This requires updating the TestBot crontab. --- testbot/bin/UpdateFailures.pl | 192 ++++++++++++++++++++++++++++++++++ testbot/doc/INSTALL.txt | 5 + 2 files changed, 197 insertions(+) create mode 100755 testbot/bin/UpdateFailures.pl
diff --git a/testbot/bin/UpdateFailures.pl b/testbot/bin/UpdateFailures.pl new file mode 100755 index 000000000..7c89b7b99 --- /dev/null +++ b/testbot/bin/UpdateFailures.pl @@ -0,0 +1,192 @@ +#!/usr/bin/perl -Tw +# -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- +# +# This script updates the failures bug information. +# +# Copyright 2022 Francois Gouget +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +use strict; + +sub BEGIN +{ + if ($0 !~ m=^/=) + { + # Turn $0 into an absolute path so it can safely be used in @INC + require Cwd; + $0 = Cwd::cwd() . "/$0"; + } + if ($0 =~ m=^(/.*)/[^/]+/[^/]+$=) + { + $::RootDir = $1; + unshift @INC, "$::RootDir/lib"; + } +} +my $Name0 = $0; +$Name0 =~ s+^.*/++; + +my $WineBugUrl = "https://bugs.winehq.org/"; + +use Text::CSV::Encoded; + +use ObjectModel::Collection; +use WineTestBot::Failures; +use WineTestBot::Log; + + +# +# Logging and error handling helpers +# + +my $Debug; +sub Debug(@) +{ + print STDERR @_ if ($Debug); +} + +my $LogOnly; +sub Error(@) +{ + print STDERR "$Name0:error: ", @_ if (!$LogOnly); + LogMsg @_; +} + + +# +# Bugzilla interface +# + +sub CollectBugInfo($) +{ + my ($BugIds) = @_; + + my $Bugs = {}; + my $Cmd = "wget -qO- '$WineBugUrl/buglist.cgi?bug_id=". join(",", @$BugIds) + ."&columnlist=bug_status,resolution,short_desc&ctype=csv'"; + if (open(my $Fh, "-|", $Cmd)) + { + <$Fh>; # skip the header line + + my $Csv = Text::CSV::Encoded->new({ binary => 1 }); + while (my $Line = <$Fh>) + { + if ($Csv->parse($Line)) + { + my ($BugId, $Status, $Resolution, $Description) = $Csv->fields(); + $Bugs->{$BugId} = { + Status => $Status, + Resolution => $Resolution, + Description => $Description, + }; + Debug("Bug $BugId - $Status - $Resolution - $Description\n"); + } + else + { + Error("could not parse line: ", $Csv->error_input, "\n"); + } + } + close($Fh); + } + return $Bugs; +} + + +# +# Main +# + +my (@FailureIds, $Usage); +while (@ARGV) +{ + my $Arg = shift @ARGV; + if ($Arg eq "--log-only") + { + $LogOnly = 1; + } + elsif ($Arg eq "--debug") + { + $Debug = 1; + } + elsif ($Arg =~ /^(?:-?|-h|--help)$/) + { + $Usage = 0; + last; + } + elsif ($Arg =~ /^([0-9]+)$/) + { + push @FailureIds, $1; + } + else + { + Error "unexpected argument '$Arg'\n"; + $Usage = 2; + last; + } +} +# Check parameters +if (defined $Usage) +{ + print "Usage: $Name0 [--log-only] [--debug] [--help] [FID...]\n"; + exit $Usage; +} + +my $Failures = CreateFailures(); +@FailureIds = @{$Failures->GetKeys()} if (!@FailureIds); +@FailureIds = sort { $a <=> $b } @FailureIds; + +# Deduplicate the bug ids +my $BugIds; +foreach my $FailureId (@FailureIds) +{ + my $Failure = $Failures->GetItem($FailureId); + $BugIds->{$Failure->BugId} = 1; +} + +# Update the failures bug information +my $Bugs = CollectBugInfo([keys %$BugIds]); +foreach my $FailureId (@FailureIds) +{ + my $Failure = $Failures->GetItem($FailureId); + my $Bug = $Bugs->{$Failure->BugId}; + if ($Bug) + { + if ($Failure->BugStatus ne "deleted") + { + my $Resolution = $Bug->{Resolution} || " ---"; + $Resolution = $Resolution eq " ---" ? "" : " $Resolution"; + $Failure->BugStatus("$Bug->{Status}$Resolution"); + } + $Failure->BugDescription($Bug->{Description}); + } + else + { + $Failure->BugStatus("Does not exist"); + $Failure->BugDescription("The WineHQ bug ". $Failure->BugId ." does not exist!"); + } + if ($Debug and $Failure->GetIsModified()) + { + Debug("Failure $FailureId - ", $Failure->BugStatus, " - ", $Failure->BugDescription, "\n"); + } +} + +my ($ErrKey, $ErrProperty, $ErrMessage) = $Failures->Save(); +if ($ErrMessage) +{ + Error("Could not save $ErrKey because of the $ErrProperty field: $ErrMessage\n"); + exit 1; +} + +exit 0; diff --git a/testbot/doc/INSTALL.txt b/testbot/doc/INSTALL.txt index 46edeea52..fac4580b2 100644 --- a/testbot/doc/INSTALL.txt +++ b/testbot/doc/INSTALL.txt @@ -81,6 +81,11 @@ Setup for Winetest updates: instance: */10 * * * * $HOME/tools/testbot/bin/CheckForWinetestUpdate.pl --log-only
+Setup for failure bug information updates: +- Use a cron job to run UpdateFailures.pl periodically. For + instance: + 0 */4 * * * $HOME/tools/testbot/bin/UpdateFailures.pl --log-only + Setup for wine-devel: - A proper WineTestBot system should integrate with Wine's patches site. An alternative is to interface directly with wine-devel as described in