• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / roku-report-analyzer / #125

07 Dec 2023 07:14PM UTC coverage: 100.0%. Remained the same
#125

push

TwitchBronBron
0.3.10

153 of 153 branches covered (100.0%)

Branch coverage included in aggregate %.

303 of 303 relevant lines covered (100.0%)

15.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/CrashlogFile.ts
1
import * as path from 'path';
1✔
2
import type { ApplicationVersionCount, CrashReport, FileReference, LocalVariable, StackFrame } from './interfaces';
3
import type { Position } from 'brighterscript';
4
import type { Runner } from './Runner';
5
import { util as bscUtil } from 'brighterscript';
1✔
6
import { util } from './util';
1✔
7

8
export class CrashlogFile {
1✔
9
    public constructor(
10
        public runner: Runner,
33✔
11
        /**
12
         * The path to the original source file
13
         */
14
        public srcPath: string
33✔
15
    ) {
16
        this.computeDestPath();
33✔
17
    }
18

19
    /**
20
     * The path (relative to outDir) where the processed version of this file should be written
21
     */
22
    public destPath: string | undefined;
23

24
    /**
25
     * The date where the crashes occurred
26
    */
27
    public date: string | undefined;
28

29
    /**
30
     * The full text contents of this file
31
     */
32
    public fileContents = '';
33✔
33

34
    /**
35
     * The list of pkg paths found in the parsed fileContents
36
     */
37
    public references: Array<FileReference> = [];
33✔
38

39
    public crashes: Array<CrashReport> = [];
33✔
40

41
    /**
42
     * Compute the dest path of this log file
43
     */
44
    private computeDestPath() {
45
        let logFileName = path.basename(this.srcPath);
33✔
46
        const dateFolderName = path.dirname(this.srcPath);
33✔
47
        const ext = path.extname(logFileName);
33✔
48
        //remove extension
49
        logFileName = logFileName.substring(0, logFileName.length - ext.length);
33✔
50
        const firmwareSeparatorPosition = logFileName.lastIndexOf('_');
33✔
51
        if (firmwareSeparatorPosition > -1) {
33✔
52
            const appName = logFileName.substring(0, firmwareSeparatorPosition);
9✔
53
            const firmware = logFileName.substring(firmwareSeparatorPosition + 1);
9✔
54
            const date = /oscrashes.(\d\d\d\d-\d\d-\d\d)/i.exec(dateFolderName)?.[1];
9✔
55
            if (date) {
9✔
56
                this.destPath = `${appName}/${date}-${firmware}${ext}`;
8✔
57
                this.date = date;
8✔
58
            }
59
        }
60
    }
61

62

63
    /**
64
     * Convert a position into an offset from the start of the file
65
     */
66
    private positionToOffset(position: Position) {
67
        //create the line/offset map if not yet created
68
        if (!this.lineOffsetMap) {
26✔
69
            this.lineOffsetMap = {};
12✔
70
            this.lineOffsetMap[0] = 0;
12✔
71
            const regexp = /(\r?\n)/g;
12✔
72
            let lineIndex = 1;
12✔
73
            let match: RegExpExecArray | null;
74
            // eslint-disable-next-line no-cond-assign
75
            while (match = regexp.exec(this.fileContents)) {
12✔
76
                this.lineOffsetMap[lineIndex++] = match.index + match[1].length;
92✔
77
            }
78
        }
79
        return this.lineOffsetMap[position.line] + position.character;
26✔
80
    }
81
    private lineOffsetMap!: Record<number, number>;
82

83
    public parse(fileContents: string) {
84
        this.fileContents = fileContents;
21✔
85
        const lines = fileContents?.split(/\r?\n/) ?? [];
21✔
86

87
        for (let i = 0; i < lines.length; i++) {
21✔
88
            const line = lines[i];
111✔
89
            this.findPkgPaths(line, i);
111✔
90
        }
91

92
        this.parseCrashes();
21✔
93
    }
94

95
    private parseCrashes() {
96
        this.crashes = [];
21✔
97
        let contents = this.fileContents;
21✔
98

99
        let crashReportBlocks = contents?.split(/\s*___+\s*/) ?? [];
21✔
100

101
        // Filter out crash reports without stack trace
102
        crashReportBlocks = crashReportBlocks.filter(block => !block.includes('StackTrace missing'));
23✔
103

104
        crashReportBlocks.forEach(crashReportBlock => {
21✔
105
            const crashReportBlockSections: Array<{ sectionType: CrashReportSectionType; lines: string[] }> = [];
23✔
106
            const blockLines = crashReportBlock.split(/\r?\n+/).map(x => x.trim());
101✔
107

108
            // Separate the block in three sections:
109
            // Hardware Platform, Application Version, and Stack Trace
110
            let currentSection: CrashReportSectionType | undefined;
111
            let foundSectionHeader = false;
23✔
112
            for (const line of blockLines) {
23✔
113
                if (/\s*count\s+Hardware Platform/.test(line)) {
101✔
114
                    currentSection = CrashReportSectionType.HardwarePlatform;
2✔
115
                    foundSectionHeader = true;
2✔
116
                } else if (/\s*count\s+Application Version/.test(line)) {
99✔
117
                    currentSection = CrashReportSectionType.ApplicationVersion;
2✔
118
                    foundSectionHeader = true;
2✔
119
                } else if (/\s*Stack Trace/.test(line)) {
97✔
120
                    currentSection = CrashReportSectionType.StackTrace;
2✔
121
                    foundSectionHeader = true;
2✔
122
                } else {
123
                    if (foundSectionHeader && currentSection && !/\s*---+\s*/.exec(line)) {
95✔
124
                        let crashReportSectionIndex = crashReportBlockSections.findIndex(x => x.sectionType === currentSection);
101✔
125

126
                        if (crashReportSectionIndex === -1) {
47✔
127
                            crashReportBlockSections.push({
6✔
128
                                sectionType: currentSection,
129
                                lines: [line]
130
                            });
131
                        } else {
132
                            crashReportBlockSections[crashReportSectionIndex].lines.push(line);
41✔
133
                        }
134
                    }
135
                }
136
            }
137

138
            // Skip processing blocks without sections, e.g: everything before the first crash
139
            if (crashReportBlockSections.length === 0) {
23✔
140
                return;
21✔
141
            }
142

143
            let crashReport: CrashReport = {
2✔
144
                applicationVersions: [],
145
                errorMessage: '',
146
                stackFrame: [],
147
                localVariables: [],
148
                count: {
149
                    total: 0,
150
                    details: []
151
                }
152
            };
153

154
            // Process each block section
155
            for (const crashReportSection of crashReportBlockSections) {
2✔
156
                switch (crashReportSection.sectionType) {
6✔
157
                    case CrashReportSectionType.HardwarePlatform:
158
                        crashReport.count.details = this.parseHardwarePlatformSection(crashReportSection.lines);
2✔
159
                        break;
2✔
160
                    case CrashReportSectionType.ApplicationVersion:
161
                        const applicationVersions = this.parseApplicationVersionSection(crashReportSection.lines);
2✔
162
                        crashReport.applicationVersions = applicationVersions;
2✔
163
                        crashReport.count.total = applicationVersions.reduce((acc, curr) => acc + curr.count, 0);
2✔
164
                        break;
2✔
165
                    case CrashReportSectionType.StackTrace:
166
                        const { errorMessage, stackFrame: stackTrace, localVariables } = this.parseStackTraceSection(crashReportSection.lines);
2✔
167
                        crashReport.errorMessage = errorMessage;
2✔
168
                        crashReport.stackFrame = stackTrace;
2✔
169
                        crashReport.localVariables = localVariables;
2✔
170
                        break;
2✔
171
                }
172
            }
173

174
            this.crashes.push(crashReport);
2✔
175
        });
176
    }
177

178
    /**
179
     * Link every reference with its original location
180
     */
181
    public async process() {
182
        await Promise.all(
13✔
183
            this.references.map(x => this.linkReference(x))
11✔
184
        );
185
    }
186

187
    /**
188
     * Look up the source location for each reference (using sourcemaps)
189
     */
190
    private async linkReference(reference: FileReference) {
191
        const locations = await this.runner.getOriginalLocations(reference?.pkgLocation);
12✔
192
        const firstLocation = locations?.[0];
12✔
193

194
        if (firstLocation) {
12✔
195
            //for now, just use the first location found
196
            reference.srcLocation = firstLocation;
7✔
197
        }
198
    }
199

200
    /**
201
     * Scan the text and find all pkg paths
202
     */
203
    private findPkgPaths(line: string, lineIndex: number) {
204
        const pattern = /(\w+:\/.*?)\((\d+)\)/g;
111✔
205
        let match: RegExpExecArray | null;
206
        // eslint-disable-next-line no-cond-assign
207
        while (match = pattern.exec(line)) {
111✔
208
            const range = bscUtil.createRange(lineIndex, match.index, lineIndex, match.index + match[0].length);
23✔
209
            this.references.push({
23✔
210
                range: range,
211
                offset: this.positionToOffset(range.start),
212
                length: match[0].length,
213
                pkgLocation: {
214
                    path: match[1],
215
                    //roku prints 1-based lines, but we store them as 0-based
216
                    line: parseInt(match[2]) - 1,
217
                    character: 0
218
                }
219
            });
220
        }
221
    }
222

223
    /**
224
     * Parses the hardware platform section.
225
     * Extracts the crash count for each platform.
226
    */
227
    public parseHardwarePlatformSection(sectionLines: string[]): CrashReport['count']['details'] {
228
        return sectionLines.filter(l => l !== '').map(line => {
20✔
229
            const [count, ...platformAsArray] = line.split(/\s+/);
13✔
230
            const platformCodeName = platformAsArray.join(' ').trim();
13✔
231
            return {
13✔
232
                count: parseInt(count),
233
                hardwarePlatform: platformCodeName
234
            };
235
        });
236
    }
237

238
    /**
239
     * Parses the application version section.
240
     * Extracts the crash count for each application version.
241
    */
242
    public parseApplicationVersionSection(lines: string[]): ApplicationVersionCount[] {
243
        const applicationVersions = lines.filter(l => l !== '').map(line => {
16✔
244
            const count = line.split(/\s+/)[0];
11✔
245
            const rawVersion = line.substring(count.length).trim();
11✔
246
            const splittedVersion = rawVersion.split(/\.|,|;/);
11✔
247

248
            let version;
249
            if (splittedVersion.length === 3 && splittedVersion.every(x => Number.isInteger(Number(x)))) {
23✔
250
                version = { major: parseInt(splittedVersion[0]), minor: parseInt(splittedVersion[1]), build: parseInt(splittedVersion[2]) };
6✔
251
            }
252

253
            return { count: parseInt(count), version: version, rawVersion: rawVersion } as ApplicationVersionCount;
11✔
254
        });
255

256
        return applicationVersions;
7✔
257
    }
258

259
    /**
260
     * Parses the stack trace section.
261
     * Extracts the error message, the backtrace and the local variables of the crash.
262
    */
263
    public parseStackTraceSection(lines: string[]): ParsedStackTraceSection {
264
        const parsedStackTraceSection: ParsedStackTraceSection = {
10✔
265
            errorMessage: '',
266
            stackFrame: [],
267
            localVariables: []
268
        };
269

270
        lines = lines.filter(l => l !== '');
73✔
271

272
        if (lines.length === 0) {
10✔
273
            return { errorMessage: '', stackFrame: [], localVariables: [] };
3✔
274
        }
275

276
        const firstLine = lines[0];
7✔
277

278
        // In case the error message is missing
279
        if (firstLine === 'Local Variables:' || firstLine === 'Backtrace:') {
7✔
280
            parsedStackTraceSection.errorMessage = '';
2✔
281
        } else {
282
            parsedStackTraceSection.errorMessage = firstLine;
5✔
283
            lines.shift();
5✔
284
        }
285

286
        const stackTraceSections: Array<{ sectionType: StackTraceSectionType; lines: string[] }> = [];
7✔
287

288
        let currentSection: StackTraceSectionType | undefined;
289
        let foundSectionHeader = false;
7✔
290

291
        // Separate the block in two sections: Backtrace and LocalVariables
292
        for (const line of lines) {
7✔
293
            if (line === 'Local Variables:') {
59✔
294
                currentSection = StackTraceSectionType.LocalVariables;
6✔
295
                foundSectionHeader = true;
6✔
296
            } else if (line === 'Backtrace:') {
53✔
297
                currentSection = StackTraceSectionType.BackTrace;
6✔
298
                foundSectionHeader = true;
6✔
299
            } else {
300
                if (foundSectionHeader && currentSection) {
47✔
301
                    let sectionIndex = stackTraceSections.findIndex(x => x.sectionType === currentSection);
50✔
302
                    if (sectionIndex === -1) {
44✔
303
                        stackTraceSections.push({
12✔
304
                            sectionType: currentSection,
305
                            lines: [line]
306
                        });
307
                    } else {
308
                        stackTraceSections[sectionIndex].lines.push(line);
32✔
309
                    }
310
                }
311
            }
312
        }
313

314
        // Process each section
315
        for (const stackTraceSection of stackTraceSections) {
7✔
316
            switch (stackTraceSection.sectionType) {
12✔
317
                case StackTraceSectionType.LocalVariables:
318
                    parsedStackTraceSection.localVariables = this.parseStackTraceLocalVariables(stackTraceSection.lines);
6✔
319
                    break;
6✔
320
                case StackTraceSectionType.BackTrace:
321
                    parsedStackTraceSection.stackFrame = this.parseStackFrames(stackTraceSection.lines);
6✔
322
                    break;
6✔
323
            }
324
        }
325

326
        return parsedStackTraceSection;
7✔
327
    }
328

329
    /**
330
     * Parses the stack frames.
331
     * Extracts the scope and location of each stack frame.
332
    */
333
    public parseStackFrames(lines: string[]): StackFrame[] {
334
        const stackTrace: StackFrame[] = [];
6✔
335

336
        let stackFrame: StackFrame = { scope: '', reference: undefined };
6✔
337

338
        for (const line of lines) {
6✔
339
            if (/#[0-9]+\s+./.exec(line)) {
23✔
340
                const [_, ...scopeAsArray] = line.split(/\s+/);
11✔
341
                stackFrame.scope = scopeAsArray.join(' ').trim();
11✔
342
            } else if (/file\/line:\s+./.exec(line)) {
12✔
343
                const [_, ...pkgLocationAsArray] = line.split(/\s+/);
11✔
344

345
                const match = /(\w+:\/.*?)\((\d+)\)/.exec(pkgLocationAsArray.join(' ').trim());
11✔
346
                if (match) {
11✔
347
                    // We need the range to calculate the reference index. To generate the range, we need the line number.
348
                    // We don't have access to the exact line number because we separated the stack trace
349
                    // at the beginning `parseCrashes()`, removing any unnecesary information.
350
                    // So we calculate it here from `this.fileContents`.
351
                    const fileContentsList = this.fileContents.split(/\r?\n/);
9✔
352
                    const originalLineIndex = fileContentsList.findIndex(l => l.includes(line));
102✔
353
                    const originalLineMatch = /(\w+:\/.*?)\((\d+)\)/.exec(fileContentsList[originalLineIndex]);
9✔
354

355
                    if (originalLineMatch) {
9✔
356
                        const range = bscUtil.createRange(originalLineIndex, originalLineMatch.index, originalLineIndex, originalLineMatch.index + originalLineMatch[0].length);
3✔
357
                        stackFrame.reference = this.references
3✔
358
                            .find(ref => util.areRangesEqual(ref.range, range) &&
10✔
359
                                ref.offset === this.positionToOffset(range.start) &&
360
                                ref.length === match[0].length);
361
                    }
362
                }
363
                // Shallow copy to avoid object reference problem.
364
                stackTrace.push({ ...stackFrame });
11✔
365
            }
366
        }
367

368
        return stackTrace;
6✔
369
    }
370

371
    /**
372
     * Parses the local variables section.
373
     * Extracts the local variables and their metadata.
374
    */
375
    public parseStackTraceLocalVariables(lines: string[]): LocalVariable[] {
376
        const localVariables: LocalVariable[] = [];
6✔
377

378
        for (const line of lines) {
6✔
379
            const [name, ...metadataAsArray] = line.split(/\s+/);
21✔
380
            const metadata = metadataAsArray.join(' ').trim();
21✔
381

382
            localVariables.push({ name: name, metadata: metadata });
21✔
383
        }
384

385
        return localVariables;
6✔
386
    }
387
}
388

389
enum CrashReportSectionType {
1✔
390
    HardwarePlatform = 'HardwarePlatform',
1✔
391
    ApplicationVersion = 'ApplicationVersion',
1✔
392
    StackTrace = 'StackTrace'
1✔
393
}
394

395
enum StackTraceSectionType {
1✔
396
    BackTrace = 'BackTrace',
1✔
397
    LocalVariables = 'LocalVariables'
1✔
398
}
399

400
interface ParsedStackTraceSection {
401
    errorMessage: string;
402
    stackFrame: StackFrame[];
403
    localVariables: LocalVariable[];
404
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc