1 /**
2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module dextool.plugin.mutate.backend.report.html.trend;
11 
12 import logger = std.experimental.logger;
13 import std.format : format;
14 
15 import arsd.dom : Element, Link, RawSource, Document;
16 
17 import dextool.plugin.mutate.backend.database : Database;
18 import dextool.plugin.mutate.backend.report.analyzers : reportTrendByCodeChange,
19     reportMutationScoreHistory, MutationScoreHistory;
20 import dextool.plugin.mutate.backend.report.html.constants;
21 import dextool.plugin.mutate.backend.type : Mutation;
22 import dextool.plugin.mutate.backend.report.html.utility : generatePopupHelp;
23 import dextool.plugin.mutate.backend.report.html.tmpl : TimeScalePointGraph;
24 
25 void makeTrend(ref Database db, string tag, Document doc, Element root) @trusted {
26     import std.datetime : SysTime;
27 
28     DashboardCss.h2(root.addChild(new Link(tag, null)).setAttribute("id", tag[1 .. $]), "Trend");
29 
30     auto base = root.addChild("div");
31 
32     addTrendGraph(db, doc, base);
33     addFileCodeChangeGraph(db, doc, base);
34     addFileScoreJsVar(db, doc, base);
35 }
36 
37 private:
38 
39 void addTrendGraph(ref Database db, Document doc, Element root) {
40     import std.conv : to;
41 
42     const history = reportMutationScoreHistory(db);
43     if (history.data.length < 2)
44         return;
45 
46     auto ts = TimeScalePointGraph("ScoreHistory");
47     foreach (v; history.rollingAvg(MutationScoreHistory.avgLong).data)
48         ts.put("Score" ~ MutationScoreHistory.avgLong.to!string,
49                 TimeScalePointGraph.Point(v.timeStamp, v.score.get));
50     ts.setColor("Score" ~ MutationScoreHistory.avgLong.to!string, "darkblue");
51     foreach (v; history.rollingAvg(MutationScoreHistory.avgShort).data)
52         ts.put("Score" ~ MutationScoreHistory.avgShort.to!string,
53                 TimeScalePointGraph.Point(v.timeStamp, v.score.get));
54     ts.setColor("Score" ~ MutationScoreHistory.avgShort.to!string, "blue");
55 
56     ts.html(root, TimeScalePointGraph.Width(80));
57 
58     generatePopupHelp(root.addChild("div", "ScoreX"),
59             "The rolling mean where X is the days it is calculated over."
60             ~ " Useful to see a trend of the test suite over a short and long term. "
61             ~ "If e.g. the long term is starting to go down then it may be time to react."
62             ~ " 'Has our teams methodology for how we work with tests degenerated?'");
63 }
64 
65 void addFileCodeChangeGraph(ref Database db, Document doc, Element root) {
66     import std.algorithm : sort, joiner;
67     import std.array : array, appender;
68     import std.range : only;
69     import std.utf : toUTF8;
70     import my.set : Set;
71     import my.path : Path;
72 
73     const codeChange = reportTrendByCodeChange(db);
74     if (codeChange.empty)
75         return;
76 
77     root.addChild("script").appendChild(new RawSource(doc,
78             `// Triggered every time a point is hovered on the ScoreByCodeChange graph
79 const change = (tooltipItems) => {
80     // Convert the X value to the date format that is used in the file_graph_score_data variable
81     var date = tooltipItems[0].xLabel.replace("T", " ");
82     date = date.substring(0,5) + toMonthShort(date.substring(5,7)) + date.substring(date.length - 13);
83 
84     var scoreList = {};
85     // Key = file_path, Value = {date : file_score}
86     for(const [key, value] of Object.entries(file_graph_score_data)){
87         if(value[date] != undefined){
88             scoreList[key] = value[date];
89         }
90     }
91 
92     // Format the string that is shown
93     var result = "";
94     var i = 0;
95     var len = Object.keys(scoreList).length;
96     for(const [key, value] of Object.entries(scoreList)){
97         result += key + " : " + value;
98         i += 1;
99         if (i < len){
100             result += "\n";
101         }
102     };
103 
104     return result;
105 };
106 `));
107 
108     auto ts = TimeScalePointGraph("ScoreByCodeChange");
109     foreach (v; codeChange.sample.byKeyValue.array.sort!((a, b) => a.key < b.key)) {
110         ts.put("Lowest score", TimeScalePointGraph.Point(v.key, v.value.min));
111     }
112     ts.setColor("Lowest score", "purple");
113     ts.html(root, TimeScalePointGraph.Width(80),
114             "ScoreByCodeChangeData['options']['tooltips']['callbacks'] = {footer:change};");
115 
116     auto info = root.addChild("div", "Code change");
117     generatePopupHelp(info,
118             "The graph is intended to help understand why the overall mutation score have changed (up/down). "
119             ~ "It may help locate the files that resulted in the change. "
120             ~ "Along the x-axis is the day when the file mutation score where last changed."
121             ~ " Multiple files that are changed on the same day are grouped together. "
122             ~ "The lowest score among the files changed for the day is plotted on the y-axis.");
123 
124     Set!Path pathIsInit;
125     auto filesData = appender!(string[])();
126     filesData.put("var file_graph_score_data = {};");
127 
128     auto scoreData = appender!(string[])();
129 
130     foreach (fileScore; codeChange.sample.byKeyValue) {
131         foreach (score; fileScore.value.points) {
132             if (score.file !in pathIsInit) {
133                 filesData.put(format!"file_graph_score_data['%s'] = {};"(score.file));
134                 pathIsInit.add(score.file);
135             }
136 
137             scoreData.put(format("file_graph_score_data['%s']['%s'] = %.3f;",
138                     score.file, fileScore.key, score.value));
139         }
140     }
141 
142     root.addChild("script").appendChild(new RawSource(doc, only(filesData.data,
143             scoreData.data).joiner.joiner("\n").toUTF8));
144 }
145 
146 void addFileScoreJsVar(ref Database db, Document doc, Element root) {
147     import std.algorithm : sort, joiner;
148     import std.array : array, appender;
149     import std.range : only;
150     import std.utf : toUTF8;
151     import miniorm : spinSql;
152     import my.set : Set;
153     import my.path : Path;
154 
155     Set!Path pathIsInit;
156     auto filesData = appender!(string[])();
157     filesData.put("var file_score_data = {};");
158 
159     auto scoreData = appender!(string[])();
160 
161     foreach (score; spinSql!(() => db.fileApi.getFileScoreHistory)) {
162         if (score.file !in pathIsInit) {
163             filesData.put(format!"file_score_data['%s'] = {};"(score.file));
164             pathIsInit.add(score.file);
165         }
166 
167         scoreData.put(format("file_score_data['%s']['%s'] = %s;", score.file,
168                 score.timeStamp, score.score.get));
169     }
170 
171     root.addChild("script").appendChild(new RawSource(doc, only(filesData.data,
172             scoreData.data).joiner.joiner("\n").toUTF8));
173 
174 }