File: makeTravisReport.m

package info (click to toggle)
matlab2tikz 1.1.0-8
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 1,668 kB
  • sloc: objc: 6,143; makefile: 55; sh: 40
file content (360 lines) | stat: -rw-r--r-- 13,733 bytes parent folder | download | duplicates (4)
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
function [nErrors] = makeTravisReport(status, varargin)
% Makes a readable report for Travis/Github of test results
%
% This function produces a testing report of HEADLESS tests for
% display on GitHub and Travis.
%
% MAKETRAVISREPORT(status) produces the report from the `status` output of
% `testHeadless`.
%
% MAKETRAVISREPORT(status, 'stream', FID, ...) changes the filestream to use
%  to output the report to. (Default: 1 (stdout)).
%
% MAKETRAVISREPORT(status, 'length', CHAR, ...) changes the report length.
%  A few values are possible that cover different aspects in less/more detail.
%    - 'default': all unreliable tests, failed & skipped tests and summary
%    - 'short'  : only show the brief summary
%    - 'long'   : all tests + summary
%
% See also: testHeadless, makeLatexReport

    SM = StreamMaker();
    %% Parse input arguments
    ipp = m2tInputParser();

    ipp = ipp.addRequired(ipp, 'status', @iscell);
    ipp = ipp.addParamValue(ipp, 'stream', 1,  SM.isStream);
    ipp = ipp.addParamValue(ipp, 'length', 'default', @isReportLength);

    ipp = ipp.parse(ipp, status, varargin{:});
    arg = ipp.Results;
    arg.length = lower(arg.length);
    stream = SM.make(arg.stream, 'w');

    %% transform status data into groups
    S = splitStatuses(status);

    %% build report
    stream.print(gfmHeader(describeEnvironment));
    reportUnreliableTests(stream, arg, S);
    reportReliableTests(stream, arg, S);
    displayTestSummary(stream, S);

    %% set output arguments if needed
    if nargout >= 1
        nErrors = countNumberOfErrors(S.reliable);
    end
end
% == INPUT VALIDATOR FUNCTIONS =================================================
function bool = isReportLength(val)
    % validates the report length
    bool = ismember(lower(val), {'default','short','long'});
end
% == GITHUB-FLAVORED MARKDOWN FUNCTIONS ========================================
function str = gfmTable(data, header, alignment)
    % Construct a Github-flavored Markdown table
    %
    % Arguments:
    %   - data: nRows x nCols cell array that represents the data
    %   - header: cell array with the (nCol) column headers
    %   - alignment: alignment specification per column
    %       * 'l': left-aligned (default)
    %       * 'c': centered
    %       * 'r': right-aligned
    %     When not enough entries are specified, the specification is repeated
    %     cyclically.
    %
    % Output: table as a string
    %
    % See https://help.github.com/articles/github-flavored-markdown/#tables

    % input argument validation and normalization
    nCols = size(data, 2);
    if ~exist('alignment','var') || isempty(alignment)
        alignment = 'l';
    end
    if numel(alignment) < nCols
        % repeat the alignment specifications along the columns
        alignment = repmat(alignment, 1, nCols);
        alignment = alignment(1:nCols);
    end

    % calculate the required column width
    cellWidth   = cellfun(@length, [header(:)' ;data]);
    columnWidth = max(max(cellWidth, [], 1),3); % use at least 3 places

    % prepare the table format
    COLSEP = '|'; ROWSEP = sprintf('\n');
    rowformat = [COLSEP sprintf([' %%%ds ' COLSEP], columnWidth) ROWSEP];
    alignmentRow = formatAlignment(alignment, columnWidth);

    % actually print the table
    fullTable = [header; alignmentRow; data];
    strs = cell(size(fullTable,1), 1);
    for iRow = 1:numel(strs)
        thisRow = fullTable(iRow,:);
        %TODO: maybe preprocess thisRow with strjust first
        strs{iRow} = sprintf(rowformat, thisRow{:});
    end
    str = [strs{:}];

    %---------------------------------------------------------------------------
    function alignRow = formatAlignment(alignment, columnWidth)
        % Construct a row of dashes to specify the alignment of each column
        % See https://help.github.com/articles/github-flavored-markdown/#tables
        DASH = '-'; COLON = ':';
        N = numel(columnWidth);
        alignRow = arrayfun(@(w) repmat(DASH, 1, w), columnWidth, ...
                        'UniformOutput', false);
        for iColumn = 1:N
            thisAlign = alignment(iColumn);
            thisSpec = alignRow{iColumn};
            switch lower(thisAlign)
                case 'l'
                    thisSpec(1) = COLON;
                case 'r'
                    thisSpec(end) = COLON;
                case 'c'
                    thisSpec([1 end]) = COLON;
                otherwise
                    error('gfmTable:BadAlignment','Unknown alignment "%s"',...
                          thisAlign);
            end
            alignRow{iColumn} = thisSpec;
        end
    end
end
function str = gfmCode(str, inline, language)
    % Construct a GFM code fragment
    %
    % Arguments:
    %   - str: code to be displayed
    %   - inline:  - true  -> formats inline
    %              - false -> formats as code block
    %              - []    -> automatic mode (default): picks one of the above
    %   - language: which language the code is (enforces a code block)
    %
    % Output: GFM formatted string
    %
    % See https://help.github.com/articles/github-flavored-markdown
    if ~exist('inline','var')
        inline = [];
    end
    if ~exist('language','var') || isempty(language)
        language = '';
    else
        inline = false; % highlighting is not supported for inline code
    end
    if isempty(inline)
        inline = isempty(strfind(str, sprintf('\n')));
    end

    if inline
        prefix = '`';
        postfix = '`';
    else
        prefix = sprintf('\n```%s\n', language);
        postfix = sprintf('\n```\n');
        if str(end) == sprintf('\n')
            postfix = postfix(2:end); % remove extra endline
        end
    end

    str = sprintf('%s%s%s', prefix, str, postfix);
end
function str = gfmHeader(str, level)
    % Constructs a GFM/Markdown header
    if ~exist('level','var')
        level = 1;
    end
    str = sprintf('\n%s %s\n', repmat('#', 1, level), str);
end
function symbols = githubEmoji()
    % defines the emojis to signal the test result
    symbols = struct('pass', ':white_check_mark:', ...
                     'fail', ':heavy_exclamation_mark:', ...
                     'skip', ':grey_question:');
end
% ==============================================================================
function S = splitStatuses(status)
    % splits a cell array of statuses into a struct of cell arrays
    % of statuses according to their value of "skip", "reliable" and whether
    % an error has occured.
    % See also: splitUnreliableTests, splitPassFailSkippedTests
    S = struct('all', {status}); % beware of cell array assignment to structs!

    [S.reliable, S.unreliable]  = splitUnreliableTests(status);
    [S.passR, S.failR, S.skipR] = splitPassFailSkippedTests(S.reliable);
    [S.passU, S.failU, S.skipU] = splitPassFailSkippedTests(S.unreliable);
end
% ==============================================================================
function [short, long] = describeEnvironment()
    % describes the environment in a short and long format
    [env, ver] = getEnvironment;
    [dummy, VCID] = VersionControlIdentifier(); %#ok
    if ~isempty(VCID)
        VCID = [' commit ' VCID(1:10)];
    end
    OS = OSVersion;
    short = sprintf('%s %s (%s)', env, ver, OS, VCID);
    long  = sprintf('Test results for m2t%s running with %s %s on %s.', ...
                    VCID, env, ver, OS);
end
% ==============================================================================
function reportUnreliableTests(stream, arg, S)
    % report on the unreliable tests
    if ~isempty(S.unreliable) && ~strcmpi(arg.length, 'short')
        stream.print(gfmHeader('Unreliable tests',2));
        stream.print('These do not cause the build to fail.\n\n');
        displayTestResults(stream, S.unreliable);
    end
end
function reportReliableTests(stream, arg, S)
    % report on the reliable tests
    switch arg.length
        case 'long'
            tests = S.reliable;
            message = '';
        case 'default'
            tests = [S.failR; S.skipR];
            message = 'Passing tests are not shown (only failed and skipped tests).\n\n';
        case 'short'
            return; % don't show this part
    end

    stream.print(gfmHeader('Reliable tests',2));
    stream.print('Only the reliable tests determine the build outcome.\n');
    stream.print(message);
    displayTestResults(stream, tests);
end
% ==============================================================================
function displayTestResults(stream, status)
    % display a table of specific test outcomes
    headers = {'Testcase', 'Name', 'OK', 'Status'};
    data = cell(numel(status), numel(headers));
    symbols = githubEmoji;
    for iTest = 1:numel(status)
        data(iTest,:) = fillTestResultRow(status{iTest}, symbols);
    end
    str = gfmTable(data, headers, 'llcl');
    stream.print('%s', str);
end
function row = fillTestResultRow(oneStatus, symbol)
    % format the status of a single test for the summary table
    testNumber = oneStatus.index;
    testSuite  = func2str(oneStatus.testsuite);
    summary = '';
    if oneStatus.skip
        summary = 'SKIPPED';
        passOrFail = symbol.skip;
    else
        stages = getStagesFromStatus(oneStatus);
        for jStage = 1:numel(stages)
            thisStage = oneStatus.(stages{jStage});
            if ~thisStage.error
                continue;
            end
            stageName = strrep(stages{jStage},'Stage','');
            switch stageName
                case 'plot'
                    summary = sprintf('%s plot failed', summary);
                case 'tikz'
                    summary = sprintf('%s m2t failed', summary);
                case 'hash'
                    summary = sprintf('new hash %32s != expected (%32s) %s', ...
                        thisStage.found, thisStage.expected, summary);
                otherwise
                    summary = sprintf('%s %s FAILED', summary, thisStage);
            end
        end
        if isempty(summary)
            passOrFail = symbol.pass;
        else
            passOrFail = symbol.fail;
        end
        summary = strtrim(summary);
    end
    row = { gfmCode(sprintf('%s(%d)', testSuite, testNumber)), ...
            gfmCode(oneStatus.function), ...
            passOrFail, ...
            summary};
end
% ==============================================================================
function displayTestSummary(stream, S)
    % display a table of # of failed/passed/skipped tests vs (un)reliable

    % compute number of cases per category
    reliableSummary   = cellfun(@numel, {S.passR, S.failR, S.skipR});
    unreliableSummary = cellfun(@numel, {S.passU, S.failU, S.skipU});

    % make summary table + calculate totals
    summary = [unreliableSummary                 numel(S.unreliable);
                 reliableSummary                 numel(S.reliable);
               reliableSummary+unreliableSummary numel(S.all)];

    % put results into cell array with proper layout
    summary = arrayfun(@(v) sprintf('%d',v), summary, 'UniformOutput', false);
    table = repmat({''}, 3, 5);
    header = {'','Pass','Fail','Skip','Total'};
    table(:,1) = {'Unreliable','Reliable','Total'};
    table(:,2:end) = summary;

    % print table
    [envShort, envDescription] = describeEnvironment(); %#ok
    stream.print(gfmHeader('Test summary', 2));
    stream.print('%s\n', envDescription);
    stream.print('%s\n', gfmCode(generateCode(S),false,'matlab'));
    stream.print(gfmTable(table, header, 'lrrrr'));

    % print overall outcome
    symbol = githubEmoji;
    nErrors = numel(S.failR);
    if nErrors == 0
        stream.print('\nBuild passes. %s\n', symbol.pass);
    else
        stream.print('\nBuild fails with %d errors. %s\n', nErrors, symbol.fail);
    end
end
function code = generateCode(S)
    % generates some MATLAB code to easily replicate the results
    code = sprintf('%s = %s;\n', ...
                   'suite', ['@' func2str(S.all{1}.testsuite)], ...
                   'alltests', testNumbers(S.all), ...
                   'reliable', testNumbers(S.reliable), ...
                   'unreliable', testNumbers(S.unreliable), ...
                   'failReliable', testNumbers(S.failR), ...
                   'passUnreliable', testNumbers(S.passU), ...
                   'skipped', testNumbers([S.skipR; S.skipU]));
    % --------------------------------------------------------------------------
    function str = testNumbers(status)
        str = intelligentVector( cellfun(@(s) s.index, status) );
    end
end
function str = intelligentVector(numbers)
    % Produce a string that is an intelligent vector notation of its arguments
    % e.g. when numbers = [ 1 2 3 4 6 7 8 9 ], it should return '[ 1:4 6:9 ]'
    % The order in the vector is not retained!

    if isempty(numbers)
        str = '[]';
    else
        numbers = sort(numbers(:).');
        delta  = diff([numbers(1)-1 numbers]);
        % place virtual bounds at the first element and beyond the last one
        bounds = [1 find(delta~=1) numel(numbers)+1];
        idx   = 1:(numel(bounds)-1); % start index of each segment
        start = numbers(bounds(idx  )  );
        stop  = numbers(bounds(idx+1)-1);
        parts = arrayfun(@formatRange, start, stop, 'UniformOutput', false);
        str = sprintf('[%s]', strtrim(sprintf('%s ', parts{:})));
    end
end
function str = formatRange(start, stop)
    % format a range [start:stop] of integers in MATLAB syntax
    if start==stop
        str = sprintf('%d',start);
    else
        str = sprintf('%d:%d',start, stop);
    end
end
% ==============================================================================