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 }