source: appstream-generator/src/asgen/reportgenerator.d @ 4841

Last change on this file since 4841 was 4841, checked in by Juanma, 2 years ago

Initial release

File size: 28.3 KB
Line 
1/*
2 * Copyright (C) 2016 Matthias Klumpp <matthias@tenstral.net>
3 *
4 * Licensed under the GNU Lesser General Public License Version 3
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation, either version 3 of the license, or
9 * (at your option) any later version.
10 *
11 * This software is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
18 */
19
20module asgen.reportgenerator;
21
22import std.stdio;
23import std.string;
24import std.parallelism;
25import std.path : buildPath, buildNormalizedPath, dirName;
26import std.file : mkdirRecurse, rmdirRecurse;
27import std.array : empty;
28import std.json;
29import std.conv : to;
30import std.typecons : scoped;
31static import std.file;
32
33import mustache;
34import appstream.Metadata;
35
36import asgen.utils;
37import asgen.config;
38import asgen.logging;
39import asgen.hint;
40import asgen.backends.interfaces;
41import asgen.datastore;
42
43
44private alias MustacheEngine!(string) Mustache;
45
46final class ReportGenerator
47{
48
49private:
50    Config conf;
51    PackageIndex pkgIndex;
52    DataStore dstore;
53
54    string htmlExportDir;
55    string templateDir;
56    string defaultTemplateDir;
57
58    string mediaPoolDir;
59    string mediaPoolUrl;
60
61    Mustache mustache;
62
63    struct HintTag
64    {
65        string tag;
66        string message;
67    }
68
69    struct HintEntry
70    {
71        string identifier;
72        string[] archs;
73        HintTag[] errors;
74        HintTag[] warnings;
75        HintTag[] infos;
76    }
77
78    struct MetadataEntry
79    {
80        ComponentKind kind;
81        string identifier;
82        string[] archs;
83        string data;
84        string iconName;
85    }
86
87    struct PkgSummary
88    {
89        string pkgname;
90        string[] cpts;
91        int infoCount;
92        int warningCount;
93        int errorCount;
94    }
95
96    struct DataSummary
97    {
98        PkgSummary[string][string] pkgSummaries;
99        HintEntry[string][string] hintEntries;
100        MetadataEntry[string][string][string] mdataEntries; // package -> version -> gcid -> entry
101        long totalMetadata;
102        long totalInfos;
103        long totalWarnings;
104        long totalErrors;
105    }
106
107public:
108
109    this (DataStore db)
110    {
111        this.conf = Config.get ();
112
113        // we need the data store to get hint and metainfo data
114        dstore = db;
115
116        htmlExportDir = conf.htmlExportDir;
117        mediaPoolDir = dstore.mediaExportPoolDir;
118        mediaPoolUrl = buildPath (conf.mediaBaseUrl, "pool");
119
120        // get template directory
121        templateDir = conf.templateDir;
122        defaultTemplateDir = buildNormalizedPath (templateDir, "..", "default");
123
124        mustache.path = templateDir;
125        mustache.ext = "html";
126    }
127
128    private string[] splitBlockData (string str, string blockType)
129    {
130        auto content = str.strip ();
131        string blockName;
132        if (content.startsWith ("{")) {
133            auto li = content.indexOf("}");
134            if (li <= 0)
135                throw new Exception ("Invalid %s: Closing '}' missing.", blockType);
136            blockName = content[1..li].strip ();
137            if (li+1 >= content.length)
138                content = "";
139            else
140                content = content[li+1..$];
141        }
142
143        if (blockName is null)
144            throw new Exception ("Invalid %s: Does not have a name.", blockType);
145
146        return [blockName, content];
147    }
148
149    private void setupMustacheContext (Mustache.Context context)
150    {
151        import std.datetime : Clock;
152
153        string[string] partials;
154
155        // this implements a very cheap way to get template inheritance
156        // would obviously be better if our template system supported this natively.
157        context["partial"] = (string str) {
158            auto split = splitBlockData (str, "partial");
159            partials[split[0]] = split[1];
160            return "";
161        };
162
163        context["block"] = (string str) {
164            auto split = splitBlockData (str, "block");
165            auto blockName = split[0];
166            str = split[1] ~ "\n";
167
168            auto partialCP = (blockName in partials);
169            if (partialCP is null)
170                return str;
171            else
172                return *partialCP;
173        };
174
175        auto time = Clock.currTime ();
176        auto timeStr = "%d-%02d-%02d %02d:%02d [%s]".format (time.year, time.month, time.day, time.hour,time.minute, time.timezone.stdName);
177
178        context["time"] = timeStr;
179        context["generator_version"] = asgen.config.generatorVersion;
180        context["project_name"] = conf.projectName;
181        context["root_url"] = conf.htmlBaseUrl;
182    }
183
184    private void renderPage (string pageID, string exportName, Mustache.Context context)
185    {
186        setupMustacheContext (context);
187
188        auto fname = buildPath (htmlExportDir, exportName) ~ ".html";
189        std.file.mkdirRecurse (dirName (fname));
190
191        if (!std.file.exists (buildPath (templateDir, pageID ~ ".html"))) {
192            if (std.file.exists (buildPath (defaultTemplateDir, pageID ~ ".html")))
193                mustache.path = defaultTemplateDir;
194        }
195
196        logDebug ("Rendering HTML page: %s", exportName);
197        auto data = mustache.render (pageID, context).strip ();
198        auto f = File (fname, "w");
199        f.writeln (data);
200
201        // reset default template path, we might have changed it
202        mustache.path = templateDir;
203    }
204
205    private void renderPagesFor (string suiteName, string section, DataSummary dsum)
206    {
207        static import std.regex;
208
209        if (templateDir is null) {
210            logError ("Can not render HTML: No page templates found.");
211            return;
212        }
213
214        logInfo ("Rendering HTML for %s/%s", suiteName, section);
215        auto maintRE = std.regex.ctRegex!(r"""[àáèéëêòöøîìùñ~/\\(\\)\" ']""", "g");
216
217        // write issue hint pages
218        foreach (ref pkgname; dsum.hintEntries.byKey ()) {
219            auto pkgHEntries = dsum.hintEntries[pkgname];
220            auto exportName = format ("%s/%s/issues/%s", suiteName, section, pkgname);
221
222            auto context = new Mustache.Context;
223            context["suite"] = suiteName;
224            context["package_name"] = pkgname;
225            context["section"] = section;
226
227            context["entries"] = (string content) {
228                string res;
229                foreach (ref cid; pkgHEntries.byKey ()) {
230                    auto hentry = pkgHEntries[cid];
231                    auto intCtx = new Mustache.Context;
232                    intCtx["component_id"] = cid;
233
234                    foreach (arch; hentry.archs) {
235                        auto archSub = intCtx.addSubContext("architectures");
236                        archSub["arch"] = arch;
237                    }
238
239                    if (!hentry.errors.empty)
240                        intCtx["has_errors"] = ["has_errors": "yes"];
241                    foreach (error; hentry.errors) {
242                        auto errSub = intCtx.addSubContext("errors");
243                        errSub["error_tag"] = error.tag;
244                        errSub["error_description"] = error.message;
245                    }
246
247                    if (!hentry.warnings.empty)
248                        intCtx["has_warnings"] = ["has_warnings": "yes"];
249                    foreach (warning; hentry.warnings) {
250                        auto warnSub = intCtx.addSubContext("warnings");
251                        warnSub["warning_tag"] = warning.tag;
252                        warnSub["warning_description"] = warning.message;
253                    }
254
255                    if (!hentry.infos.empty)
256                        intCtx["has_infos"] = ["has_infos": "yes"];
257                    foreach (info; hentry.infos) {
258                        auto infoSub = intCtx.addSubContext("infos");
259                        infoSub["info_tag"] = info.tag;
260                        infoSub["info_description"] = info.message;
261                    }
262
263                    res ~= mustache.renderString (content, intCtx);
264                }
265
266                return res;
267            };
268
269            renderPage ("issues_page", exportName, context);
270        }
271
272        // write metadata info pages
273        foreach (ref pkgname; dsum.mdataEntries.byKey ()) {
274            auto pkgMVerEntries = dsum.mdataEntries[pkgname];
275            auto exportName = format ("%s/%s/metainfo/%s", suiteName, section, pkgname);
276
277            auto context = new Mustache.Context;
278            context["suite"] = suiteName;
279            context["package_name"] = pkgname;
280            context["section"] = section;
281
282            context["cpts"] = (string content) {
283                string res;
284                foreach (ver; pkgMVerEntries.byKey ()) {
285                    auto mEntries = pkgMVerEntries[ver];
286
287                    foreach (gcid; mEntries.byKey ()) {
288                        auto mentry = mEntries[gcid];
289
290                        auto intCtx = new Mustache.Context;
291                        intCtx["component_id"] = format ("%s - %s", mentry.identifier, ver);
292
293                        foreach (arch; mentry.archs) {
294                            auto archSub = intCtx.addSubContext("architectures");
295                            archSub["arch"] = arch;
296                        }
297                        intCtx["metadata"] = mentry.data;
298
299                        auto cptMediaPath = buildPath (mediaPoolDir, gcid);
300                        auto cptMediaUrl = buildPath (mediaPoolUrl, gcid);
301                        string iconUrl;
302                        switch (mentry.kind) {
303                            case ComponentKind.UNKNOWN:
304                                iconUrl = buildPath (conf.htmlBaseUrl, "static", "img", "no-image.png");
305                                break;
306                            case ComponentKind.DESKTOP_APP:
307                            case ComponentKind.WEB_APP:
308                            case ComponentKind.FONT:
309                                if (std.file.exists (buildPath (cptMediaPath, "icons", "64x64", mentry.iconName)))
310                                    iconUrl = buildPath (cptMediaUrl, "icons", "64x64", mentry.iconName);
311                                else
312                                    iconUrl = buildPath (conf.htmlBaseUrl, "static", "img", "no-image.png");
313                                break;
314                            default:
315                                iconUrl = buildPath (conf.htmlBaseUrl, "static", "img", "cpt-nogui.png");
316                                break;
317                        }
318
319                        intCtx["icon_url"] = iconUrl;
320
321                        res ~= mustache.renderString (content, intCtx);
322                    }
323
324                }
325
326                return res;
327            };
328
329            renderPage ("metainfo_page", exportName, context);
330        }
331
332        // write hint overview page
333        auto hindexExportName = format ("%s/%s/issues/index", suiteName, section);
334        auto hsummaryCtx = new Mustache.Context;
335        hsummaryCtx["suite"] = suiteName;
336        hsummaryCtx["section"] = section;
337
338        hsummaryCtx["summaries"] = (string content) {
339            string res;
340
341            foreach (maintainer; dsum.pkgSummaries.byKey ()) {
342                auto summaries = dsum.pkgSummaries[maintainer];
343                auto intCtx = new Mustache.Context;
344                intCtx["maintainer"] = maintainer;
345                intCtx["maintainer_anchor"] = std.regex.replaceAll (maintainer, maintRE, "_");
346
347                bool interesting = false;
348                foreach (summary; summaries.byValue ()) {
349                    if ((summary.infoCount == 0) && (summary.warningCount == 0) && (summary.errorCount == 0))
350                        continue;
351                    interesting = true;
352                    auto maintSub = intCtx.addSubContext("packages");
353                    maintSub["pkgname"] = summary.pkgname;
354
355                    // again, we use this dumb hack to allow conditionals in the Mustache
356                    // template.
357                    if (summary.infoCount > 0)
358                        maintSub["has_info_count"] =["has_count": "yes"];
359                    if (summary.warningCount > 0)
360                        maintSub["has_warning_count"] =["has_count": "yes"];
361                    if (summary.errorCount > 0)
362                        maintSub["has_error_count"] =["has_count": "yes"];
363
364                    maintSub["info_count"] = summary.infoCount;
365                    maintSub["warning_count"] = summary.warningCount;
366                    maintSub["error_count"] = summary.errorCount;
367                }
368
369                if (interesting)
370                    res ~= mustache.renderString (content, intCtx);
371            }
372
373            return res;
374        };
375        renderPage ("issues_index", hindexExportName, hsummaryCtx);
376
377        // write metainfo overview page
378        auto mindexExportName = format ("%s/%s/metainfo/index", suiteName, section);
379        auto msummaryCtx = new Mustache.Context;
380        msummaryCtx["suite"] = suiteName;
381        msummaryCtx["section"] = section;
382
383        msummaryCtx["summaries"] = (string content) {
384            string res;
385
386            foreach (maintainer; dsum.pkgSummaries.byKey ()) {
387                auto summaries = dsum.pkgSummaries[maintainer];
388                auto intCtx = new Mustache.Context;
389                intCtx["maintainer"] = maintainer;
390                intCtx["maintainer_anchor"] = std.regex.replaceAll (maintainer, maintRE, "_");
391
392                intCtx["packages"] = (string content) {
393                    string res;
394                    foreach (summary; summaries) {
395                        if (summary.cpts.length == 0)
396                            continue;
397                        auto subCtx = new Mustache.Context;
398                        subCtx["pkgname"] = summary.pkgname;
399
400                        foreach (cid; summary.cpts) {
401                            auto cptsSub = subCtx.addSubContext("components");
402                            cptsSub["cid"] = cid;
403                        }
404
405                        res ~= mustache.renderString (content, subCtx);
406                    }
407
408                    return res;
409                };
410
411                res ~= mustache.renderString (content, intCtx);
412            }
413
414            return res;
415        };
416        renderPage ("metainfo_index", mindexExportName, msummaryCtx);
417
418        // render section index page
419        auto secIndexExportName = format ("%s/%s/index", suiteName, section);
420        auto secIndexCtx = new Mustache.Context;
421        secIndexCtx["suite"] = suiteName;
422        secIndexCtx["section"] = section;
423
424        float percOne = 100.0 / cast(float) (dsum.totalMetadata + dsum.totalInfos + dsum.totalWarnings + dsum.totalErrors);
425        secIndexCtx["valid_percentage"] =  dsum.totalMetadata * percOne;
426        secIndexCtx["info_percentage"] = dsum.totalInfos * percOne;
427        secIndexCtx["warning_percentage"] = dsum.totalWarnings * percOne;
428        secIndexCtx["error_percentage"] = dsum.totalErrors * percOne;
429
430        secIndexCtx["metainfo_count"] = dsum.totalMetadata;
431        secIndexCtx["error_count"] = dsum.totalErrors;
432        secIndexCtx["warning_count"] = dsum.totalWarnings;
433        secIndexCtx["info_count"] = dsum.totalInfos;
434
435
436        renderPage ("section_overview", secIndexExportName, secIndexCtx);
437    }
438
439    private DataSummary preprocessInformation (string suiteName, string section, Package[] pkgs)
440    {
441        DataSummary dsum;
442
443        logInfo ("Collecting data about hints and available metainfo for %s/%s", suiteName, section);
444        auto hintstore = HintsStorage.get ();
445
446        auto dtype = conf.metadataType;
447        auto mdata = scoped!Metadata ();
448        mdata.setFormatStyle (FormatStyle.COLLECTION);
449        mdata.setFormatVersion (conf.formatVersion);
450
451        foreach (ref pkg; pkgs) {
452            immutable pkid = pkg.id;
453
454            auto gcids = dstore.getGCIDsForPackage (pkid);
455            auto hintsData = dstore.getHints (pkid);
456            if ((hintsData is null) && (gcids is null))
457                continue;
458
459            PkgSummary pkgsummary;
460            bool newInfo = false;
461
462            pkgsummary.pkgname = pkg.name;
463            if (pkg.maintainer in dsum.pkgSummaries) {
464                auto pkgSumP = pkg.name in dsum.pkgSummaries[pkg.maintainer];
465                if (pkgSumP !is null)
466                    pkgsummary = *pkgSumP;
467                else
468                    newInfo = true;
469            }
470
471            // process component metadata for this package if there are any
472            if (gcids !is null) {
473                foreach (gcid; gcids) {
474                    auto cid = getCidFromGlobalID (gcid);
475
476                    // don't add the same entry multiple times for multiple versions
477                    if (pkg.name in dsum.mdataEntries) {
478                        if (pkg.ver in dsum.mdataEntries[pkg.name]) {
479                            auto meP = gcid in dsum.mdataEntries[pkg.name][pkg.ver];
480                            if (meP is null) {
481                                // this component is new
482                                dsum.totalMetadata += 1;
483                                newInfo = true;
484                            } else {
485                                // we already have a component with this gcid
486                                (*meP).archs ~= pkg.arch;
487                                continue;
488                            }
489                        }
490                    } else {
491                        // we will add a new component
492                        dsum.totalMetadata += 1;
493                    }
494
495                    MetadataEntry me;
496                    me.identifier = cid;
497                    me.data = dstore.getMetadata (dtype, gcid);
498
499                    mdata.clearComponents ();
500                    if (dtype == DataType.YAML)
501                        mdata.parse (me.data, FormatKind.YAML);
502                    else
503                        mdata.parse (me.data, FormatKind.XML);
504                    auto cpt = mdata.getComponent ();
505
506                    if (cpt !is null) {
507                        auto iconsArr = cpt.getIcons ();
508                        for (uint i = 0; i < iconsArr.len; i++) {
509                            import appstream.Icon;
510                            auto icon = scoped!Icon (cast (AsIcon*) iconsArr.index (i));
511
512                            if (icon.getKind () == IconKind.CACHED) {
513                                me.iconName = icon.getName ();
514                                break;
515                            }
516                        }
517
518                        me.kind = cpt.getKind ();
519                    } else {
520                        me.kind = ComponentKind.UNKNOWN;
521                    }
522
523                    me.archs ~= pkg.arch;
524                    dsum.mdataEntries[pkg.name][pkg.ver][gcid] = me;
525                    pkgsummary.cpts ~= format ("%s - %s", cid, pkg.ver);
526                }
527            }
528
529            // process hints for this package, if there are any
530            if (hintsData !is null) {
531                auto hintsCpts = parseJSON (hintsData);
532                hintsCpts = hintsCpts["hints"];
533
534                foreach (cid; hintsCpts.object.byKey ()) {
535                    auto jhints = hintsCpts[cid];
536
537                    HintEntry he;
538                    // don't add the same hints multiple times for multiple versions and architectures
539                    if (pkg.name in dsum.hintEntries) {
540                        auto heP = cid in dsum.hintEntries[pkg.name];
541                        if (heP !is null) {
542                            he = *heP;
543                            // we already have hints for this component ID
544                            he.archs ~= pkg.arch;
545
546                            // TODO: check if we have the same hints - if not, create a new entry.
547                            continue;
548                        }
549
550                        newInfo = true;
551                    } else {
552                        newInfo = true;
553                    }
554
555                    he.identifier = cid;
556
557                    foreach (jhint; jhints.array) {
558                        auto tag = jhint["tag"].str;
559                        auto hdef = hintstore.getHintDef (tag);
560                        if (hdef.tag is null) {
561                            logError ("Encountered invalid tag '%s' in component '%s' of package '%s'", tag, cid, pkid);
562
563                            // emit an internal error, invalid tags shouldn't happen
564                            hdef = hintstore.getHintDef ("internal-unknown-tag");
565                            assert (hdef.tag !is null);
566                            jhint["vars"] = ["tag": tag];
567                        }
568
569                        // render the full message using the static template and data from the hint
570                        auto context = new Mustache.Context;
571                        foreach (var; jhint["vars"].object.byKey ()) {
572                            context[var] = jhint["vars"][var].str;
573                        }
574                        auto msg = mustache.renderString (hdef.text, context);
575
576                        // add the new hint to the right category
577                        auto severity = hintstore.getSeverity (tag);
578                        if (severity == HintSeverity.INFO) {
579                            he.infos ~= HintTag (tag, msg);
580                            pkgsummary.infoCount++;
581                        } else if (severity == HintSeverity.WARNING) {
582                            he.warnings ~= HintTag (tag, msg);
583                            pkgsummary.warningCount++;
584                        } else {
585                            he.errors ~= HintTag (tag, msg);
586                            pkgsummary.errorCount++;
587                        }
588                    }
589
590                    if (newInfo)
591                        he.archs ~= pkg.arch;
592
593                    dsum.hintEntries[pkg.name][he.identifier] = he;
594                }
595            }
596
597            dsum.pkgSummaries[pkg.maintainer][pkg.name] = pkgsummary;
598            if (newInfo) {
599                dsum.totalInfos += pkgsummary.infoCount;
600                dsum.totalWarnings += pkgsummary.warningCount;
601                dsum.totalErrors += pkgsummary.errorCount;
602            }
603        }
604
605        // rehash the tables for slightly better performance
606        dsum.hintEntries.rehash;
607        dsum.mdataEntries.rehash;
608        dsum.pkgSummaries.rehash;
609
610        return dsum;
611    }
612
613    private void saveStatistics (string suiteName, string section, DataSummary dsum)
614    {
615        auto stat = JSONValue (["suite": JSONValue (suiteName),
616                                "section": JSONValue (section),
617                                "totalInfos": JSONValue (dsum.totalInfos),
618                                "totalWarnings": JSONValue (dsum.totalWarnings),
619                                "totalErrors": JSONValue (dsum.totalErrors),
620                                "totalMetadata": JSONValue (dsum.totalMetadata)]);
621        dstore.addStatistics (stat);
622    }
623
624    void exportStatistics ()
625    {
626        import std.algorithm : sort;
627
628        logInfo ("Exporting statistical data.");
629
630        // return all statistics we have from the database
631        auto statsCollection = dstore.getStatistics ();
632
633        auto emptyJsonObject ()
634        {
635            auto jobj = JSONValue (["null": 0]);
636            jobj.object.remove ("null");
637            return jobj;
638        }
639
640        auto emptyJsonArray ()
641        {
642            auto jarr = JSONValue ([0, 0]);
643            jarr.array = [];
644            return jarr;
645        }
646
647        // create JSON for use with e.g. Rickshaw graph
648        auto smap = emptyJsonObject ();
649
650        foreach (ref entry; statsCollection) {
651            auto js = entry.data;
652            immutable timestamp = entry.time;
653            JSONValue jstats;
654            if (js.type == JSON_TYPE.ARRAY)
655                jstats = js;
656            else
657                jstats = JSONValue ([js]);
658
659            foreach (ref jvals; jstats.array) {
660                auto suite = jvals["suite"].str;
661                auto section = jvals["section"].str;
662
663                if (suite !in smap)
664                    smap.object[suite] = emptyJsonObject ();
665                if (section !in smap[suite]) {
666                    smap[suite].object[section] = emptyJsonObject ();
667                    auto sso = smap[suite][section].object;
668                    sso["errors"] = emptyJsonArray ();
669                    sso["warnings"] = emptyJsonArray ();
670                    sso["infos"] = emptyJsonArray ();
671                    sso["metadata"] = emptyJsonArray ();
672                }
673                auto suiteSectionObj = smap[suite][section].object;
674
675                auto pointErr = JSONValue ([JSONValue (timestamp), JSONValue (jvals["totalErrors"])]);
676                suiteSectionObj["errors"].array ~= pointErr;
677
678                auto pointWarn = JSONValue ([JSONValue (timestamp), JSONValue (jvals["totalWarnings"])]);
679                suiteSectionObj["warnings"].array ~= pointWarn;
680
681                auto pointInfo = JSONValue ([JSONValue (timestamp), JSONValue (jvals["totalInfos"])]);
682                suiteSectionObj["infos"].array ~= pointInfo;
683
684                auto pointMD = JSONValue ([JSONValue (timestamp), JSONValue (jvals["totalMetadata"])]);
685                suiteSectionObj["metadata"].array ~= pointMD;
686            }
687        }
688
689        bool compareJData (JSONValue x, JSONValue y) @trusted
690        {
691            size_t xv;
692            size_t yv;
693            if (x.array[0].type == JSON_TYPE.UINTEGER)
694                xv = to!size_t (x.array[0].uinteger);
695            else
696                xv = to!size_t (x.array[0].integer);
697
698            if (y.array[0].type == JSON_TYPE.UINTEGER)
699                yv = to!size_t (y.array[0].uinteger);
700            else
701                yv = to!size_t (y.array[0].integer);
702
703            return xv < yv;
704        }
705
706        // ensure our data is sorted ascending by X
707        foreach (suite; smap.object.byKey ()) {
708            foreach (section; smap[suite].object.byKey ()) {
709                auto sso = smap[suite][section].object;
710
711                sort!(compareJData) (sso["errors"].array);
712                sort!(compareJData) (sso["warnings"].array);
713                sort!(compareJData) (sso["infos"].array);
714                sort!(compareJData) (sso["metadata"].array);
715            }
716        }
717
718        auto fname = buildPath (htmlExportDir, "statistics.json");
719        mkdirRecurse (dirName (fname));
720
721        auto sf = File (fname, "w");
722        sf.writeln (toJSON (&smap, false));
723        sf.flush ();
724        sf.close ();
725    }
726
727    void processFor (string suiteName, string section, Package[] pkgs)
728    {
729        // collect all needed information and save statistics
730        auto dsum = preprocessInformation (suiteName, section, pkgs);
731        saveStatistics (suiteName, section, dsum);
732
733        // drop old pages
734        auto suitSecPagesDest = buildPath (htmlExportDir, suiteName, section);
735        if (std.file.exists (suitSecPagesDest))
736            rmdirRecurse (suitSecPagesDest);
737
738        // render fresh info pages
739        renderPagesFor (suiteName, section, dsum);
740    }
741
742    void updateIndexPages ()
743    {
744        logInfo ("Updating HTML index pages and static data.");
745        // render main overview
746        auto context = new Mustache.Context;
747        foreach (suite; conf.suites) {
748            auto sub = context.addSubContext("suites");
749            sub["suite"] = suite.name;
750
751            auto secCtx = new Mustache.Context;
752            secCtx["suite"] = suite.name;
753            foreach (section; suite.sections) {
754                auto secSub = secCtx.addSubContext("sections");
755                secSub["section"] = section;
756            }
757            renderPage ("sections_index", format ("%s/index", suite.name), secCtx);
758        }
759
760        foreach (suite; conf.oldsuites) {
761            auto sub = context.addSubContext("oldsuites");
762            sub["suite"] = suite;
763        }
764
765        renderPage ("main", "index", context);
766
767        // copy static data, if present
768        auto staticSrcDir = buildPath (templateDir, "static");
769        if (std.file.exists (staticSrcDir)) {
770            auto staticDestDir = buildPath (htmlExportDir, "static");
771            if (std.file.exists (staticDestDir))
772                rmdirRecurse (staticDestDir);
773            copyDir (staticSrcDir, staticDestDir);
774        }
775    }
776}
777
778unittest
779{
780    writeln ("TEST: ", "Report Generator");
781
782    //auto rg = new ReportGenerator (null);
783    //rg.renderIndices ();
784}
Note: See TracBrowser for help on using the repository browser.