TestingSuite: Long overdue revival.
[testingSuite] / testSuite.ec
1 import "ecere"
2 import "EDA"
3 import "md5"
4 import "Project"
5
6 IDESettings ideSettings;
7
8 IDESettingsContainer settingsContainer
9 {
10    dataOwner = &ideSettings;
11    dataClass = class(IDESettings);
12 };
13
14 IDEConfigHolder ideConfig { };
15
16 Test dummy; // FIXME ugly hack
17
18 GlobalSettings globalSettings { };
19
20 struct MD5Digest
21 {
22    byte digest [16];
23
24    void OnDisplay(Surface surface,
25                   int x, int y, int width,
26                   void * fieldData,
27                   Alignment alignment,
28                   DataDisplayFlags flags)
29    {
30       ColorKey keys[] = {{Color {digest[0] , digest[1] , digest[2] }, 0.0f},
31                          {Color {digest[3] , digest[4] , digest[5] }, 0.2f},
32                          {Color {digest[6] , digest[7] , digest[8] }, 0.4f},
33                          {Color {digest[9] , digest[10], digest[11]}, 0.6f},
34                          {Color {digest[12], digest[13], digest[14]}, 0.8f},
35                          {Color {digest[15], digest[0] , digest[1] }, 1.0f}};
36
37       surface.Gradient(keys, 6, 0, horizontal, x+1, y+1, x+34, y+14);
38       surface.Rectangle(x+1,y+1,x+34,y+14);
39    }
40
41    void OnSerialize(IOChannel channel)
42    {
43       channel.WriteData(digest,16);
44    }
45    void OnUnserialize(IOChannel channel)
46    {
47       channel.ReadData(digest,16);
48    }
49    property TestRun
50    {
51       set {
52          TempFile hashTemp { };
53
54          Array<OutputFile> files = AccessMacros::allOutputFiles(value);
55          for(of : files)
56          {
57             File f = of.outputFile;
58             if(f)
59             {
60                while(!f.Eof())
61                {
62                   byte buffer[1024];
63                   uint count = f.Read(buffer,1,sizeof(buffer));
64                   hashTemp.Write(buffer, 1, count);
65                }
66                delete f;
67             }
68          }
69          {
70             uint size;
71             MD5_CTX ctx;
72             MD5Init(&ctx);
73             size = (uint)hashTemp.GetSize();
74             MD5Update(&ctx, hashTemp.buffer, size);
75             MD5Final(digest, &ctx);
76          }
77          delete hashTemp;
78          files.Free();
79          delete files;
80       }
81       // TOFIX: When this is commented out, no warning at conversion but conversion not acting as such!!
82       get { return 0; }
83    }
84 };
85
86 CompilerConfig defaultCompiler;
87
88 void CreateMakefile(String epjPath, String makePath)
89 {
90    Project project;
91    bool valid = true;
92    char* configName = null;//"release";
93
94    if(false) //noGlobalSettings)
95    {
96       defaultCompiler = MakeDefaultCompiler("Default", true);
97       delete ideSettings;
98    }
99    else
100    {
101       const char * compiler = getenv("COMPILER");
102       if(!compiler) compiler = "Default";
103
104 // TODO: Command line option to choose between the two
105 // or a command line option to not use global settings
106 //defaultCompiler = MakeDefaultCompiler();
107       defaultCompiler = ideConfig.compilers.GetCompilerConfig(compiler);
108 // possible TODO: use the workspace to select the active compiler
109 // TODO: option to specify compiler name when using global settings
110    }
111    defaultCompiler.eccCommand = "ecc -nolinenumbers";
112
113    if(FileExists(epjPath).isFile)
114    {
115       char extension[MAX_EXTENSION] = "";
116       GetExtension(epjPath, extension);
117       strlwr(extension);
118       if(!strcmp(extension, ProjectExtension))
119       {
120          project = LoadProject(epjPath, "Debug");
121          if(project)
122          {
123             ProjectConfig defaultConfig = null;
124             if(configName)
125             {
126                valid = false;
127                for(config : project.configurations)
128                {
129                   if(!strcmpi(configName, config.name))
130                   {
131                      project.config = config;
132                      valid = true;
133                      break;
134                   }
135                }
136                if(!valid)
137                   printf("Error: Project configuration (%s) was not found.\n", configName);
138             }
139             else
140             {
141                ProjectConfig releaseConfig;
142                for(config : project.configurations)
143                {
144                   if(!strcmpi(config.name, "Release"))
145                   {
146                      project.config = config;
147                      releaseConfig = config;
148                      break;
149                   }
150                }
151                if(!releaseConfig && project.configurations.count)
152                {
153                   project.config = project.configurations.firstIterator.data;
154                   /*
155                   releaseConfig = project.configs.first;
156                   releaseConfig.objDir.dir = "release";
157                   releaseConfig.targetDir.dir = "release";
158                   releaseConfig.optimize = forSpeed;
159                   releaseConfig.debug = false;
160                   releaseConfig.name = "Release";
161                   */
162                }
163
164                if(!releaseConfig)
165                {
166                   printf("Error: There are no Project configurations.\n");
167                   valid = false;
168                   // we don't need to create a config to compile a config-less project, do we?
169                   /*
170                   char targetName[MAX_FILENAME];
171                   GetLastDirectory(epjPath, targetName);
172                   StripExtension(targetName);
173                   defaultConfig = ProjectConfig
174                   {
175                      name = "Release";
176                      targetType = executable;
177                      targetName = targetName;
178                      defaultNameSpace = "";
179                      objDir.dir = "release";
180                      targetDir.dir = "release";
181                      optimize = forSpeed;
182                      debug = false;
183                      allWarnings = true;
184                      makingModified = true;
185                      compilingModified = true;
186                      linkingModified = true;
187                   };
188                   project.config = defaultConfig;
189                   */
190                }
191             }
192             if(valid)
193             {
194                project.GenerateCrossPlatformMk(null);
195                project.GenerateCompilerCf(defaultCompiler, true);
196                project.GenerateMakefile(makePath, false, null, project.config);
197             }
198
199             delete defaultConfig;
200             delete project;
201          }
202       }
203    }
204 }
205
206
207 dbtable "Tests" Test
208 {
209    Test       id         "id";
210    String     name       "name";
211    String     filePath   "filePath";
212    TestRun    reference  "reference";
213    bool       active     "active";
214 }
215
216 enum OutputFileType
217 {
218    sym, imp, c, ec
219 };
220
221 dbtable "OutputFiles" OutputFile
222 {
223    OutputFile     id         "id";
224    File           outputFile "outputFile";
225    String         filePath   "filePath";
226    Test           test       "test";
227    TestRun        run        "run";
228    OutputFileType type       "type";
229    DateTime       created    "created";
230    dbindex run;
231 }
232
233 dbtable "Runs" TestRun
234 {
235    TestRun   id              "id";
236    File      makeOutput      "makeOutput";
237    int       makeReturnValue "makeReturnValue";
238    Test      test            "test";
239    TimeStamp time            "time";
240    bool      save            "save";
241    MD5Digest digest          "digest";
242    dbindex test, time testAndTime;
243 }
244
245 Database db;
246
247 class AccessMacros
248 {
249    TestRun ::lastTestRun(Test t)
250    {
251       TestRun tr = 0;
252       Row tRow { tbl = dbindex("Runs", testAndTime) };
253       if(tRow.Find(dbfield("Runs",test),middle,nil,t)) {
254          tr = tRow.sysID;
255       }
256       delete tRow;
257       return tr;
258    }
259    Array<TestRun> ::allTestRuns(Test t)
260    {
261       Array<TestRun> runs { };
262       Row tRow { tbl = dbindex("Runs",testAndTime) };
263       for(tRow.Find(dbfield("Runs",test),middle,nil,t); !tRow.nil; tRow.Find(dbfield("Runs", test), next, nil, t)) //Next())
264          runs.Add(tRow.sysID);
265       delete tRow;
266       return runs;
267    }
268    Array<OutputFile> ::allOutputFiles(TestRun t)
269    {
270       Array<OutputFile> files { };
271       RowOutputFiles tRow { tbl = dbindex("OutputFiles",run) };
272       for(tRow.Find(dbfield("OutputFiles",run),middle,nil,t); !tRow.nil; tRow.Find(dbfield("OutputFiles", run), next, nil, t)) //tRow.Next())
273       {
274          if(t != tRow.run)
275             break;
276          files.Add(tRow.sysID);
277       }
278       delete tRow;
279       return files;
280    }
281 }
282
283 class TestSuiteGlobalSettings : GlobalAppSettings
284 {
285    settingsName = "testSuite";
286    char * diffTool;
287    char * testDB_file;
288    ~TestSuiteGlobalSettings()
289    {
290       delete diffTool;
291       delete testDB_file;
292    }
293    void OnAskReloadSettings()
294    {
295       Load();
296    }
297    public SettingsIOResult Load()
298    {
299       SettingsIOResult result = GlobalAppSettings::Load();
300       if(result == success)
301       {
302          delete diffTool;
303          delete testDB_file;
304          GetGlobalValue("Tools","diff", singleString, &diffTool);
305          GetGlobalValue("Tests","list", singleString, &testDB_file);
306          CloseAndMonitor();
307       }
308       return result;
309    }
310
311    public SettingsIOResult Save()
312    {
313       SettingsIOResult result = GlobalAppSettings::Save();
314       if(result == success)
315       {
316          PutGlobalValue("Tools","diff", singleString, diffTool);
317          PutGlobalValue("Tests","list", singleString, testDB_file);
318          CloseAndMonitor();
319       }
320       return result;
321    }
322 }
323
324 class DiffThread : Thread
325 {
326    char origDir[MAX_LOCATION];
327    char newDir[MAX_LOCATION];
328
329    uint Main()
330    {
331       DualPipe diff;
332       FileListing flOrig { origDir, 0 };
333       FileListing flNew { newDir, 0 };
334       diff = DualPipeOpenf({ true }, "%s \"%s\" \"%s\"", theGlobalSettings.diffTool, origDir, newDir);
335       if(diff)
336       {
337          diff.Wait();
338       }
339       while(flOrig.Find())
340       {
341          DeleteFile(flOrig.path);
342       }
343       RemoveDir(origDir);
344       while(flNew.Find())
345       {
346          DeleteFile(flNew.path);
347       }
348       RemoveDir(newDir);
349       delete diff;
350       return 0;
351    }
352    void DiffDir(Array<OutputFile> outputOrig, Array<OutputFile> outputNew)
353    {
354       //char origDir[MAX_LOCATION];
355       //char newDir[MAX_LOCATION];
356       CreateTemporaryDir(origDir, "TestSuiteOrigDir");
357       CreateTemporaryDir(newDir,"TestSuiteNewDir");
358
359       for(d : [ outputOrig, outputNew ])
360       {
361          for(of : d)
362          {
363             char tmpname[MAX_LOCATION];
364             char dummybuffer[MAX_LOCATION];
365             File outputFile = of.outputFile;
366             if(outputFile)
367             {
368                File tmp;
369                GetLastDirectory(of.filePath, dummybuffer);
370                strcpy(tmpname, (d == outputOrig) ? origDir : newDir);
371                PathCat(tmpname, dummybuffer);
372                tmp = FileOpen(tmpname, write);
373                if(tmp)
374                {
375                   CopyFile(outputFile, tmp);
376                   delete tmp;
377                }
378             }
379             delete outputFile;
380          }
381       }
382
383       Create();
384    }
385 }
386
387 class BuildTool
388 {
389    int GenerateMakefile(Test t)
390    {
391       //DualPipe epj2makePipe;
392       char testFolder[MAX_LOCATION];
393       char makefileOut[MAX_LOCATION];
394       char epjName[MAX_LOCATION];
395       char * filePath = t.filePath;
396
397       /*
398       StripLastDirectory(filePath, testFolder);
399       PathCat(testFolder, "release");
400       MakeDir(testFolder);
401       */
402
403       StripLastDirectory(filePath, testFolder);
404       GetLastDirectory(filePath, epjName);
405       strcpy(makefileOut, testFolder);
406       PathCat(makefileOut, "project-testingSuite.Makefile");
407
408       // epj2makePipe = DualPipeOpenf({ output = true },"epj2make -l /usr/ecere/lib -o %s %s > /dev/null", makefileOut, t.filePath);
409       //epj2makePipe = DualPipeOpenf({ output = true },"epj2make -cpp cpp -l /usr/ecere/lib -o %s %s", makefileOut, filePath);
410       //epj2makePipe.Wait();
411       CreateMakefile(filePath, makefileOut);
412
413       delete filePath;
414       //return epj2makePipe.GetExitCode();
415       return 0;
416    }
417
418    char* Copy2Temp(File f, String tmpPath)
419    {
420       File tmp;
421       char tmpFilename[MAX_LOCATION];
422       tmp = CreateTemporaryFile(tmpFilename, tmpPath);
423       CopyFile(f, tmp);
424       tmp.Flush();
425       return CopyString(tmpFilename);
426    }
427
428    int BuildTest(TestRun run)
429    {
430       DualPipe makePipe;
431       int makeSuccess = -1;
432       char makefileOut[MAX_LOCATION];
433       char * filePath = run.test.filePath;
434       char oldDir[MAX_LOCATION];
435       StripLastDirectory(filePath, makefileOut);
436
437       GetWorkingDir(oldDir, sizeof(oldDir));
438       ChangeWorkingDir(makefileOut);
439       PathCat(makefileOut, "project-testingSuite.Makefile");
440
441       //Execute("make -f %s clean", makefileOut);
442
443       makePipe = DualPipeOpenf({ output = true },"%s -f %s clean", defaultCompiler.makeCommand, makefileOut);
444       if(makePipe)
445       {
446          TempFile f { };
447          char makeBuffer[1024];
448          uint count;
449          while(!makePipe.Eof())
450          {
451             count = makePipe.Read(makeBuffer,1,sizeof(makeBuffer));
452             f.Write(makeBuffer, 1, count);
453          }
454          run.makeOutput = f;
455          delete makePipe;
456       }
457
458       makePipe = DualPipeOpenf({ output = true },"%s -f %s", defaultCompiler.makeCommand, makefileOut);
459       if(makePipe)
460       {
461          TempFile f { };
462          char makeBuffer[1024];
463          uint count;
464          while(!makePipe.Eof())
465          {
466             count = makePipe.Read(makeBuffer,1,sizeof(makeBuffer));
467             f.Write(makeBuffer, 1, count);
468          }
469          run.makeOutput = f;
470          //makePipe.Wait();
471          ChangeWorkingDir(oldDir);
472          makeSuccess = makePipe.GetExitCode();
473          run.makeReturnValue = makeSuccess;
474          delete makePipe;
475       }
476       return makeSuccess;
477    }
478    void AddOutputFiles(TestRun run)
479    {
480       char testFolder[MAX_LOCATION];
481       FileListing fl;
482       char * filePath = run.test.filePath;
483       StripLastDirectory(filePath, testFolder);
484
485       // TODO: Get from config
486       PathCat(testFolder, "obj/release.win32");
487
488       fl = { testFolder, extensions = "c, ec, sym, imp" }; // ["c", "ec", "sym", "imp"] perhaps?
489       while(fl.Find())
490       {
491          File f;
492          RowOutputFiles of { };
493          char extension[MAX_LOCATION];
494          DateTime rightNow { };
495          f = FileOpen(fl.path, read);
496
497          of.Add();
498          of.id = of.sysID;
499          of.outputFile = f;
500          of.filePath = fl.path;
501          of.test = run.test;
502          of.run = run;
503          GetExtension(fl.path, extension);
504          if(!strcmp(extension,"c"))
505          {
506             of.type = c;
507          }
508          else if(!strcmp(extension,"ec"))
509          {
510             of.type = ec;
511          }
512          else if(!strcmp(extension,"sym"))
513          {
514             of.type = sym;
515          }
516          else if(!strcmp(extension,"imp"))
517          {
518             of.type = imp;
519          }
520          rightNow.GetLocalTime();
521          of.created = (TimeStamp)rightNow;
522          delete f;
523       }
524       delete filePath;
525    }
526 }
527
528 void CopyFile(File input, File output)
529 {
530    byte buffer[65536];
531    input.Seek(0, start);
532    for(;!input.Eof();)
533    {
534       uint count = input.Read(buffer, 1, sizeof(buffer));
535       if(count)
536       {
537          output.Write(buffer, 1, count);
538       }
539    }
540 }
541
542 class RelativeTimeStamp : TimeStamp
543 {
544    const char * OnGetString(char * tempString, void * fieldData, bool * needClass)
545    {
546       int64 count;
547       int days;
548       const String units;
549       DateTime dt { };
550       TimeStamp rightNow;
551       dt.GetLocalTime();
552       rightNow = (TimeStamp)dt;
553       count = (int64)(rightNow - (TimeStamp)this);
554       if(count >= 60)
555       {
556          count /= 60;
557          if(count >= 60)
558          {
559             count /= 60;
560             if(count >= 24)
561             {
562                count /= 24;
563                if(count >= 7)
564                {
565                   days = (int)count;
566                   count /= 7;
567                   if(days >= 30)
568                   {
569                      count = days / 30;
570                      if(days >= 365)
571                      {
572                         count = days / 365;
573                         units = "year";
574                      }
575                      else
576                      {
577                         units = "month";
578                      }
579                   }
580                   else
581                   {
582                      units = "week";
583                   }
584                }
585                else
586                {
587                   units = "day";
588                }
589             }
590             else
591             {
592                units = "hour";
593             }
594          }
595          else
596          {
597             units = "minute";
598          }
599       }
600       else
601       {
602          count = 0;
603       }
604       if(count == 0)
605       {
606          PrintBuf(tempString, 1024, "<1 minute ago");
607       }
608       else
609       {
610          PrintBuf(tempString, 1024, count, " ", units, (count > 1)? "s" : "", " ago");
611       }
612       return tempString;
613    }
614 }
615
616 enum BuildStatus
617 {
618    success, failure;
619    void OnDisplay(Surface surface,
620                   int x, int y, int width,
621                   void * unused,
622                   Alignment alignment,
623                   DataDisplayFlags flags)
624    {
625       Bitmap b;
626       if(this==success)
627       {
628          b = theTestSuiteWindow.makeSuccess.bitmap;
629       }
630       else
631       {
632          b = theTestSuiteWindow.makeFailure.bitmap;
633       }
634       surface.Blit(b, x, y, 0, 0, b.width, b.height);
635    }
636 };
637
638 class TestSuiteWindow : Window
639 {
640    text = "Ecere Compiler Tests";
641    background = activeBorder;
642    borderStyle = sizable;
643    hasMaximize = true;
644    hasMinimize = true;
645    hasClose = true;
646    size = { 520, 600 };
647    anchor = { horz = -3, vert = 80 };
648
649    BitmapResource makeSuccess { ":check.png" , window = this };
650    BitmapResource makeFailure { ":x.png", window = this };
651
652    TestSuiteWindow()
653    {
654       testListBox.AddField(fieldName);
655       testListBox.AddField(fieldActive);
656
657       runsListBox.AddField(fieldBuildStatus);
658       runsListBox.AddField(fieldAuraColor);
659       runsListBox.AddField(fieldRun);
660       runsListBox.AddField(fieldSaved);
661       // LoadTests(); FIXME this causes a segfault (but not OnCreate)
662       Print("starting up...\n");
663    }
664
665    PaneSplitter windowPanes
666    {
667       this, leftPane = testListBox, rightPane = runSection, split = 200;
668    };
669    Window runSection
670    {
671       this, anchor = { top = 0, bottom = 30, left = 0, right = 0 };
672    };
673    PaneSplitter runPanes
674    {
675       runSection, leftPane = dataBox, rightPane = runsListBox, split = 100, orientation = horizontal;
676    };
677
678    Timer timer
679    {
680       this,
681       delay = 30,
682       started = true;
683
684       bool DelayExpired()
685       {
686          runsListBox.Update(null);
687          return true;
688       }
689    };
690
691    void NewTest(RowTests tRow, const char * testName, const char * testFilePath)
692    {
693       tRow.Add();
694       tRow.id = tRow.sysID;
695       tRow.active=true;
696       tRow.name=testName;
697       tRow.filePath = testFilePath;
698    }
699
700    DataRow AddTest(RowTests tRow)
701    {
702       DataRow row;
703       String s;
704       row = testListBox.AddRow();
705       row.tag = tRow.id;
706       s = tRow.name; row.SetData(fieldName, s); delete s;
707       row.SetData(fieldActive, tRow.active);
708       return row;
709    }
710
711    void DeleteTest(ListBox testListBox, DataRow row)
712    {
713       // do we also delete all runs and files associated with this test?
714       RowTests tRow { };
715       tRow.sysID=(uint)row.tag;
716       tRow.Delete();
717       delete tRow; // won't be needed in the (distant?) future :-)
718       testListBox.DeleteRow(row);
719    }
720
721    void LoadTests()
722    {
723       RowTests tRow { };
724       while(tRow.Next())
725       {
726          theTestSuiteWindow.AddTest(tRow);
727       }
728       delete tRow;
729    }
730
731    DataField fieldName   { width = 140, header = "Name"  , dataType = class(char*), editable = true };
732    DataField fieldActive { width = 40,  header = "Active", dataType = class(bool)                   };
733
734    ListBox testListBox
735    {
736       this,
737       anchor = { top = 2, bottom = 30, left = 2, right = 2 },
738       hasHeader = true;
739
740       bool NotifyChanged(ListBox testListBox, DataRow row)
741       {
742          RowTests tRow { };
743          tRow.sysID=(uint)row.tag;
744          tRow.name = row.GetData(fieldName);
745          tRow.active = row.GetData(fieldActive);
746          delete tRow;
747          return true;
748       }
749
750       bool NotifyKeyDown(ListBox testListBox, DataRow row, Key key, unichar ch)
751       {
752          if(key == del)
753          {
754             DeleteTest(testListBox, row);
755             // bRemove.NotifyClicked(this, bRemove,0,0,0);
756          }
757          else if(key == space)
758          {
759             //bool newActiveStatus = !(*(bool*)row.GetData(fieldActive)); // FIXME is needed?
760             row.SetData(fieldActive, !row.GetData(fieldActive));
761             testListBox.NotifyChanged(this, testListBox, row);
762             return false;
763          }
764          return true;
765       }
766
767       bool NotifySelect(ListBox listBox, DataRow row, Modifiers mods)
768       {
769          dataBox.Clear();
770          if(row)
771          {
772             { // print to data box
773                Test t;
774                char* tname;
775                char* tpath;
776                bool tactive;
777                t = (Test)row.tag;
778                tname = t.name;
779                tpath = t.filePath;
780                tactive = t.active;
781                dataBox.Printf("Name: %s\n", tname);
782                dataBox.Printf("Location: %s\n", tpath);
783                dataBox.Printf("Active: %d\n", tactive);
784                delete tname; delete tpath;
785             }
786             { // show test runs
787                Test t;
788                Array<TestRun> runs;
789                t = (Test)row.tag;
790                runs = AccessMacros::allTestRuns(t);
791                runsListBox.Clear();
792                for(tr : runs)
793                {
794                   DataRow row;
795                   BuildStatus buildStatus;
796                   TimeStamp time = tr.time;
797                   MD5Digest aura = tr.digest;
798                   buildStatus = tr.makeReturnValue ? failure : success;
799                   row = runsListBox.AddRow();
800                   row.tag = tr;
801                   row.SetData(fieldBuildStatus, buildStatus);
802                   row.SetData(fieldAuraColor, aura);
803                   row.SetData(fieldRun, time);
804                   row.SetData(fieldSaved, tr.save);
805                }
806             }
807          }
808          return true;
809       }
810    };
811
812    DataField fieldBuildStatus { width = 40,  dataType = class(BuildStatus),       header = "Make" };
813    DataField fieldAuraColor   { width = 40,  dataType = class(MD5Digest),         header = "Aura" };
814    DataField fieldRun         { width = 120, dataType = class(RelativeTimeStamp), header = "Run" };
815    DataField fieldSaved       { width = 80,  dataType = class(bool),              header = "Save", editable = true };
816
817    ListBox runsListBox
818    {
819       parent = runSection,
820       master=this,
821       anchor = { top = 0, bottom = 0, left = 0, right = 0 },
822       hasHeader = true;
823
824       bool NotifyChanged(ListBox listBox, DataRow row)
825       {
826          TestRun tr = (TestRun)row.tag;
827          tr.save = row.GetData(fieldSaved);
828          return true;
829       }
830    };
831    EditBox dataBox
832    {
833       parent = runSection,
834       master=this,
835       opacity = 0,
836       inactive = true,
837       anchor = { top = 0, bottom = 0, left = 0, right = 0 },
838       hasHorzScroll = true,
839       readOnly = true,
840       multiLine = true,
841       noCaret = true
842    };
843    Button addTest
844    {
845       parent=this,
846       text = "Add",
847       hotKey=altA,
848       anchor = { bottom = 2, left = 2 };
849
850       bool NotifyClicked(Button button, int x, int y, Modifiers mods)
851       {
852          FileFilter projectFilters[] =
853             {
854                { "eC project and source files (.ec, .epj)", "ec, epj" }
855             };
856          FileDialog f { filters = projectFilters, sizeFilters = sizeof(projectFilters), type = multiOpen };
857          incref f;
858          if(f.Modal()== ok)
859          {
860             int i;
861             RowTests tRow { };
862             for(i=0; i<f.numSelections; ++i)
863             {
864                char testName[MAX_LOCATION];
865                DataRow row;
866                GetLastDirectory(f.multiFilePaths[i], testName);
867                NewTest(tRow, testName, f.multiFilePaths[i]);
868                row = AddTest(tRow);
869                if(i+1 == f.numSelections)
870                {
871                   testListBox.SelectRow(row);
872                }
873             }
874          }
875          delete f;
876          //Refocus();
877          return true;
878       }
879    };
880    Button runTests
881    {
882       parent = this,
883       text = "Run",
884       hotKey = altR,
885       anchor = { bottom = 2, right = 2 };
886
887       bool NotifyClicked(Button button, int x, int y, Modifiers mods)
888       {
889          DataRow row;
890          for(row=testListBox.firstRow; row; row=row.next)
891          {
892             Test t = (Test)row.tag;
893             if(!t.active)
894             {
895                continue;
896             }
897             else
898             {
899                BuildTool bt { };
900                DateTime rightNow { };
901                RowRuns runs { };
902                TestRun run;
903                runs.Add();
904                runs.id = runs.sysID;
905                rightNow.GetLocalTime();
906                runs.time = (TimeStamp)rightNow;
907                runs.test = t;
908                run = runs.id;
909                bt.GenerateMakefile(t);
910                bt.BuildTest(run);
911                bt.AddOutputFiles(run);
912                runs.digest = run;
913                if(!t.reference)
914                {
915                   t.reference = run;
916                }
917                else
918                {
919                   DiffThread dt { };
920                   Array<OutputFile> outputsLatest = AccessMacros::allOutputFiles(run);
921                   Array<OutputFile> outputsReference = AccessMacros::allOutputFiles(t.reference);
922                   dt.DiffDir(outputsReference, outputsLatest);
923                }
924             }
925             testListBox.NotifySelect(this, testListBox, testListBox.currentRow, mods);
926          }
927          return true;
928       }
929    };
930    Button acceptTest
931    {
932       parent=this,
933       text = "Accept Run (x)",
934       hotKey=altX,
935       anchor={ bottom = 2, right = 52 };
936
937       bool NotifyClicked(Button button, int x, int y, Modifiers mods)
938       {
939          DataRow row;
940          MessageBox confirmation
941          {
942             type = yesNo,
943             text = "Accept Test",
944             contents = "Accept the selected test?"
945          };
946          if(confirmation.Modal() == yes)
947          {
948             TestRun tr;
949             row = runsListBox.currentRow;
950             if(row)
951             {
952                tr = (TestRun)row.tag;
953                tr.save = true;
954                tr.test.reference = tr;
955             }
956          }
957          return true;
958       }
959    };
960
961    bool OnCreate()
962    {
963       globalSettings.Load();
964
965       LoadTests();
966       delete theGlobalSettings.diffTool;
967       theGlobalSettings.diffTool = CopyString("meld");
968       // theGlobalSettings.diffTool = CopyString("C:/Program Files/Araxis/Araxis Merge v6.5/compare.exe /wait");
969       return true;
970    }
971 }
972
973 TestSuiteWindow theTestSuiteWindow { };
974 TestSuiteGlobalSettings theGlobalSettings { };
975
976 class TestSuiteApp : GuiApplication
977 {
978    DataSource ds { driver = /*"SQLite" */"EDB" };
979    TestSuiteApp()
980    {
981       //File f { };
982       //f.OnSerialize(0);
983       db = database_open(ds, "TestSuite");
984    }
985    ~TestSuiteApp()
986    {
987       Array<OutputFile> files;
988       Array<TestRun> runs;
989       RowTests tests { };
990       RowRuns rRuns { };
991       RowOutputFiles rFiles { };
992       while(tests.Next())
993       {
994          Test t = (Test)tests.id;
995          runs = AccessMacros::allTestRuns(t);
996          for(run : runs)
997          {
998             if(!run.save)
999             {
1000                if(t.reference == run)
1001                   t.reference = 0;
1002                files = AccessMacros::allOutputFiles(run);
1003                for(file : files)
1004                {
1005                   rFiles.sysID=file;
1006                   rFiles.Delete();
1007                }
1008                rRuns.sysID=run;
1009                rRuns.Delete();
1010             }
1011          }
1012       }
1013       delete tests;
1014       delete rRuns;
1015       delete rFiles;
1016       delete db;
1017       delete ds;
1018    }
1019
1020    bool Init()
1021    {
1022       settingsContainer.Load();
1023       ideConfig.compilers.read(settingsContainer);
1024       delete settingsContainer;
1025       return true;
1026    }
1027 }