-
Notifications
You must be signed in to change notification settings - Fork 31
/
CodeChecker.m
268 lines (231 loc) · 11.2 KB
/
CodeChecker.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
classdef CodeChecker < handle
% CODECHECKER - Class for evaluating code from ChatGPT
% CodeChecker is a class that can extract code from ChatGPT
% responses, run the code with a unit testing framework, and return
% the test results in a format for display in a chat window. Each
% input message from ChatGPT gets its own test folder to hold the
% generated code and artifacts like test results and images
%
% Example:
%
% % Get a message from ChatGPT
% myBot = chatGPT();
% response = send(myBot,"Create a bar plot in MATLAB.");
%
% % Create object
% checker = CodeChecker(response);
%
% % Check code for validity and display test report
% runChecks(checker);
% report = checker.Report;
properties (SetAccess=private)
ChatResponse % ChatGPT response that may have code
OutputFolder % Full path with test results
Results % Table with code check results
Timestamp % Unique string with current time to use for names
Artifacts % List of files generated by the checks
ErrorMessages % Cell str of any error messages from results
Report % String with the nicely formatted results
end
methods
%% Constructor
function obj = CodeChecker(inputStr,pathStr)
%CODECHECKER Constructs an instance of this class
% checker = CodeChecker(message) creates an instance
% of the CodeChecker class given a response from
% ChatGPT in the string "message".
arguments
inputStr string {mustBeTextScalar}
pathStr string {mustBeTextScalar} = "";
end
obj.ChatResponse = inputStr;
if pathStr == ""
s = dir;
pathStr = s(1).folder;
end
% Construct a unique output folder name using a timstamp
obj.Timestamp = string(datetime('now','Format','yyyy-MM-dd''T''HH-mm-SS'));
obj.OutputFolder = fullfile(pathStr,"contents","GeneratedCode","Test-" + obj.Timestamp);
% Empty Results table
obj.Results = [];
end
end
methods (Access=public)
function runChecks(obj)
% RUNCHECKS - Run generated code and check for errors
% runChecks(obj) will run each piece of generated code. NOTE:
% This function does not check generated code for
% correctness, just validity
% make the output folder
mkdir(obj.OutputFolder)
% Get list of current files so we know which artifacts to move
% to the output folder
filesBefore = dir();
% save code files and get their names for testing;
saveCodeFiles(obj);
% Run all test files
runCodeFiles(obj);
% Move all newly generated files
filesAfter = dir();
obj.Artifacts = string(setdiff({filesAfter.name},{filesBefore.name}));
for i = 1:length(obj.Artifacts)
movefile(obj.Artifacts(i),obj.OutputFolder)
end
% Fill the results properties like Report and ErrorMessages
processResults(obj)
end
end
methods(Access=private)
function saveCodeFiles(obj)
% SAVECODEFILES Saves M-files for testing
% saveCodeFiles(obj) will parse the ChatResponse propety for
% code blocks and save them to separate M-files with unique
% names in the OutputFolder property location
% Extract code blocks
[startTag,endTag] = TextHelper.codeBlockPattern();
codeBlocks = extractBetween(obj.ChatResponse,startTag,endTag);
% Create Results table
obj.Results = table('Size',[length(codeBlocks) 4],'VariableTypes',["string","string","string","logical"], ...
'VariableNames',["ScriptName","ScriptContents","ScriptOutput","IsError"]);
obj.Results.ScriptContents = codeBlocks;
% Save code blocks to M-files
for i = 1:height(obj.Results)
% Open file with the function name or a generic test name.
obj.Results.ScriptName(i) = "Test" + i + "_" + replace(obj.Timestamp,"-","_");
fid = fopen(obj.Results.ScriptName(i) + ".m","w");
% Add the code to the file
fprintf(fid,"%s",obj.Results.ScriptContents(i));
fclose(fid);
end
end
function runCodeFiles(obj)
% RUNCODEFILES - Tries to run all the generated scripts and
% captures outputs/figures
% Before tests, make existing figure handles invisible
figsBefore = findobj('Type','figure');
for i = 1:length(figsBefore)
figsBefore(i).HandleVisibility = "off";
end
% Also check for open and closed Simulink files
addOns = matlab.addons.installedAddons;
addOns = addOns(addOns.Name=="Simulink",:);
hasSimulink = ~isdeployed && ~isempty(addOns) && addOns.Enabled;
if hasSimulink
BDsBefore = find_system('SearchDepth',0);
SLXsBefore = dir("*.slx");
SLXsBefore = {SLXsBefore.name}';
end
% Iterate through scripts. Run the code and capture any output
for i = 1:height(obj.Results)
try
output = evalc(obj.Results.ScriptName(i));
if isempty(output)
output = "This code did not produce any output";
end
obj.Results.ScriptOutput(i) = output;
catch ME
obj.Results.IsError(i) = true;
obj.Results.ScriptOutput(i) = TextHelper.shortErrorReport(ME);
end
end
% Find newly Simulink models
if hasSimulink
BDsNew = setdiff(find_system("SearchDepth",0),BDsBefore);
BDsNew(BDsNew=="simulink") = [];
SLXsfiles = dir("*.slx");
SLXsNew = setdiff({SLXsfiles.name}',SLXsBefore);
SLXsNew = extractBefore(SLXsNew,".slx");
% Load the new SLX-files. Add to list of new BDs
for i = 1:length(SLXsNew)
load_system(SLXsNew{i});
end
BDsNew = union(BDsNew,SLXsNew);
% Print screenshots for the new BDs. Then save and close them
for i = 1:length(BDsNew)
print("-s" + BDsNew{i},"-dpng",BDsNew{i} + ".png");
save_system(BDsNew{i});
close_system(BDsNew{i});
end
end
% Save new figures as png and reset the old figures as visible
figsAfter = findobj("Type","figure");
for i = 1:length(figsAfter)
saveas(figsAfter(i),"Figure" + i + ".png");
end
for i = 1:length(figsBefore)
figsBefore(i).HandleVisibility = "on";
end
end
function processResults(obj)
% PROCESSRESULTS - Process the test results
%
% processResults(obj) will assign values to the properties
% Report and ErrorMessages. obj.Report is a string with a
% nicely formatted report, and obj.ErrorMessages is a cellstr
% with any error messages
%
% Get the error messages from the Results table
obj.ErrorMessages = obj.Results.ScriptOutput(obj.Results.IsError);
% Set up the report header
numBlocks = height(obj.Results);
numErrors = length(obj.ErrorMessages);
reportHeader = sprintf(['<div class="test-report"><p>Here are the test results. ' ...
'There were <b>%d code blocks</b> tested and <b>%d errors</b>.</p>'], ...
numBlocks,numErrors);
% Handle plurals
if numBlocks == 1
reportHeader = replace(reportHeader,'were','was');
reportHeader = replace(reportHeader,'blocks','block');
end
if numErrors == 1
reportHeader = replace(reportHeader,'errors','error');
end
% Loop through results table to make the report
testReport = reportHeader;
for i = 1:height(obj.Results)
% Start the report with the script name
testReport = [testReport sprintf('<div class="test"><p><b>Test:</b> %s</p>', ...
obj.Results.ScriptName(i))]; %#ok<AGROW>
% Use error style if code produced an error
codeBlockClass = "test-block-green";
if obj.Results.IsError(i)
codeBlockClass = "test-block-red";
end
% Add code
testReport = [testReport sprintf('<code class=%s>%%%% Code: \n\n%s\n\n', ...
codeBlockClass,obj.Results.ScriptContents(i))]; %#ok<AGROW>
% Add output
testReport = [testReport sprintf('%%%% Command Window Output: \n\n%s', ...
obj.Results.ScriptOutput(i))]; %#ok<AGROW>
% Add the closing tags
testReport = [testReport '</code></div>']; %#ok<AGROW>
end
% Show any image artifacts
imageFiles = obj.Artifacts(contains(obj.Artifacts,".png"));
folders = split(obj.OutputFolder,filesep);
if ~isempty(imageFiles)
testReport = [testReport '<div class="figures"><p><b>Figures</b></p>'];
for i = 1:length(imageFiles)
% Get the relative path of the image. Assumes that the HTML
% file is at the same level as the "GeneratedCode" folder
relativePath = fullfile(folders(end-1),folders(end),imageFiles(i));
relativePath = replace(relativePath,'\','/');
% Assemble the html code for displaying the image
testReport = [testReport sprintf('<div class="figure"><img src="%s" class="ml-figure"/></div>',relativePath)]; %#ok<AGROW>
end
testReport = [testReport '</div>'];
end
% List the artifacts
testReport = [testReport '<div class="artifacts"><p><b>Artifacts</b></p>'];
testReport = [testReport sprintf('<code class="code-block">The following artifacts were saved to: %s\n\n',obj.OutputFolder)];
for i = 1:length(obj.Artifacts)
testReport = [testReport sprintf(' %s\n',obj.Artifacts(i))]; %#ok<AGROW>
end
testReport = [testReport '</code></div>'];
% Close the initial div for the overall report
testReport = [testReport '</div>'];
% Assign testReport to Report property
obj.Report = testReport;
end
end
end