|
1 | | -import { getInput, setFailed, setOutput } from '@actions/core'; |
| 1 | +import {getInput, setFailed, setOutput} from '@actions/core'; |
2 | 2 | import xmlParser from 'fast-xml-parser'; |
3 | 3 | import fs from 'fs'; |
4 | 4 |
|
5 | 5 | type TestTimes = { [name: string]: number }; |
6 | 6 |
|
7 | 7 | function getTestSuiteTimesFromXml(testReportsDir: string): TestTimes { |
8 | 8 |
|
9 | | - const options = { |
10 | | - ignoreAttributes: false |
11 | | - }; |
12 | | - const parser = new xmlParser.XMLParser(options); |
13 | | - const testTimes: TestTimes = {}; |
14 | | - try { |
15 | | - fs.readdirSync(testReportsDir).forEach(file => { |
16 | | - if (file.endsWith('.xml')) { |
17 | | - const path = `${testReportsDir}/${file}`; |
18 | | - const XMLdata = fs.readFileSync(path); |
19 | | - const parsed = parser.parse(XMLdata); |
20 | | - const testSuiteName = parsed.testsuite['@_name']; |
21 | | - const testSuiteTime = parseFloat(parsed.testsuite['@_time']); |
22 | | - testTimes[testSuiteName] = testSuiteTime; |
23 | | - } |
24 | | - }); |
25 | | - } catch (err) { |
26 | | - console.error(`Warning: could not read test reports from ${testReportsDir}: ${err}`); |
27 | | - } |
28 | | - |
29 | | - return testTimes; |
| 9 | + const options = { |
| 10 | + ignoreAttributes: false |
| 11 | + }; |
| 12 | + const parser = new xmlParser.XMLParser(options); |
| 13 | + const testTimes: TestTimes = {}; |
| 14 | + try { |
| 15 | + fs.readdirSync(testReportsDir).forEach(file => { |
| 16 | + if (file.endsWith('.xml')) { |
| 17 | + try { |
| 18 | + console.log(`Parsing xml report ${file}`) |
| 19 | + const path = `${testReportsDir}/${file}`; |
| 20 | + const XMLdata = fs.readFileSync(path); |
| 21 | + const parsed = parser.parse(XMLdata); |
| 22 | + const testSuiteName = parsed.testsuite['@_name']; |
| 23 | + const testSuiteTime = parseFloat(parsed.testsuite['@_time']); |
| 24 | + testTimes[testSuiteName] = testSuiteTime; |
| 25 | + } catch (e) { |
| 26 | + console.error(`Failed to parse xml report ${file}`) |
| 27 | + } |
| 28 | + } |
| 29 | + }); |
| 30 | + } catch (err) { |
| 31 | + console.error(`Warning: could not read test reports from ${testReportsDir}: ${err}`); |
| 32 | + } |
| 33 | + |
| 34 | + return testTimes; |
30 | 35 | } |
31 | 36 |
|
32 | 37 | function estimateTestTimes(testTimes: TestTimes, testNames: string[]): TestTimes { |
33 | | - let maxTestTime = Math.max(...Object.values(testTimes)); |
34 | | - // If maxTestTime is zero, i.e. no runtimes are known, we assign everything an |
35 | | - // arbitrary non-zero value of 1.0 |
36 | | - maxTestTime = Math.max(maxTestTime, 1.0); |
37 | | - |
38 | | - const estimatedTestTimes: TestTimes = {}; |
39 | | - testNames.forEach(testName => { |
40 | | - estimatedTestTimes[testName] = testTimes[testName] || maxTestTime; |
41 | | - // Scalatest actually reported occasionally test times with negative numbers, |
42 | | - // so we set it to zero in that case. |
43 | | - estimatedTestTimes[testName] = Math.max(estimatedTestTimes[testName], 0.0); |
44 | | - }); |
45 | | - |
46 | | - return estimatedTestTimes |
| 38 | + let maxTestTime = Math.max(...Object.values(testTimes)); |
| 39 | + // If maxTestTime is zero, i.e. no runtimes are known, we assign everything an |
| 40 | + // arbitrary non-zero value of 1.0 |
| 41 | + maxTestTime = Math.max(maxTestTime, 1.0); |
| 42 | + |
| 43 | + const estimatedTestTimes: TestTimes = {}; |
| 44 | + testNames.forEach(testName => { |
| 45 | + estimatedTestTimes[testName] = testTimes[testName] || maxTestTime; |
| 46 | + // Scalatest actually reported occasionally test times with negative numbers, |
| 47 | + // so we set it to zero in that case. |
| 48 | + estimatedTestTimes[testName] = Math.max(estimatedTestTimes[testName], 0.0); |
| 49 | + }); |
| 50 | + |
| 51 | + return estimatedTestTimes |
47 | 52 | } |
48 | 53 |
|
49 | 54 | function splitTests(sortedTestNames: string[], estimatedTestTimes: TestTimes, splitTotal: number): string[][] { |
50 | | - const bucketTimes = Array(splitTotal).fill(0); |
51 | | - const buckets = Array.from(Array(splitTotal), () => new Array()) |
| 55 | + const bucketTimes = Array(splitTotal).fill(0); |
| 56 | + const buckets = Array.from(Array(splitTotal), () => new Array()) |
52 | 57 |
|
53 | 58 |
|
54 | | - sortedTestNames.forEach(testName => { |
55 | | - const minBucketIndex = bucketTimes.indexOf(Math.min(...bucketTimes)); |
56 | | - bucketTimes[minBucketIndex] += estimatedTestTimes[testName]; |
57 | | - buckets[minBucketIndex].push(testName); |
| 59 | + sortedTestNames.forEach(testName => { |
| 60 | + const minBucketIndex = bucketTimes.indexOf(Math.min(...bucketTimes)); |
| 61 | + bucketTimes[minBucketIndex] += estimatedTestTimes[testName]; |
| 62 | + buckets[minBucketIndex].push(testName); |
58 | 63 |
|
59 | | - console.log(`added ${testName} to bucket ${minBucketIndex}, total time: ${bucketTimes[minBucketIndex]}`); |
60 | | - console.log(`bucket ${minBucketIndex} has ${buckets[minBucketIndex].length} tests`); |
61 | | - }); |
| 64 | + console.log(`added ${testName} to bucket ${minBucketIndex}, total time: ${bucketTimes[minBucketIndex]}`); |
| 65 | + console.log(`bucket ${minBucketIndex} has ${buckets[minBucketIndex].length} tests`); |
| 66 | + }); |
62 | 67 |
|
63 | | - return buckets; |
| 68 | + return buckets; |
64 | 69 | } |
65 | 70 |
|
66 | 71 | function computeBuckets(testReportsDir: string, testNamesFile: string, splitTotal: number) { |
67 | | - const testTimes = getTestSuiteTimesFromXml(testReportsDir); |
| 72 | + const testTimes = getTestSuiteTimesFromXml(testReportsDir); |
68 | 73 |
|
69 | | - const testNames = fs.readFileSync(testNamesFile).toString().split('\n').filter(name => name.length > 0); |
| 74 | + const testNames = fs.readFileSync(testNamesFile).toString().split('\n').filter(name => name.length > 0); |
70 | 75 |
|
71 | | - const estimatedTestTimes = estimateTestTimes(testTimes, testNames); |
| 76 | + const estimatedTestTimes = estimateTestTimes(testTimes, testNames); |
72 | 77 |
|
73 | | - // Build a sorted list of test names, sorted by their estimated test time. |
74 | | - // We first sort alphabetically, so that tests with the same estimated time |
75 | | - // are sorted in a deterministic way. |
76 | | - const sortedTestNames = testNames.sort().sort((a, b) => estimatedTestTimes[a] - estimatedTestTimes[b]); |
| 78 | + // Build a sorted list of test names, sorted by their estimated test time. |
| 79 | + // We first sort alphabetically, so that tests with the same estimated time |
| 80 | + // are sorted in a deterministic way. |
| 81 | + const sortedTestNames = testNames.sort().sort((a, b) => estimatedTestTimes[a] - estimatedTestTimes[b]).reverse(); |
77 | 82 |
|
78 | | - const buckets = splitTests(sortedTestNames, estimatedTestTimes, splitTotal); |
| 83 | + const buckets = splitTests(sortedTestNames, estimatedTestTimes, splitTotal); |
79 | 84 |
|
80 | | - buckets.forEach((bucket, i) => { |
81 | | - console.log(`bucket ${i}: ${bucket.length} tests, total time: ${bucket.reduce((acc, testName) => acc + estimatedTestTimes[testName], 0)}`); |
82 | | - }); |
83 | | - return buckets; |
| 85 | + buckets.forEach((bucket, i) => { |
| 86 | + console.log(`bucket ${i}: ${bucket.length} tests, total time: ${bucket.reduce((acc, testName) => acc + estimatedTestTimes[testName], 0)}`); |
| 87 | + }); |
| 88 | + return buckets; |
84 | 89 | } |
85 | 90 |
|
86 | 91 | const buckets = computeBuckets(getInput('test_reports_dir'), getInput('test_names_file'), parseInt(getInput('split_total'))); |
|
0 commit comments