Skip to content

Commit 417cf78

Browse files
authored
test: improve tests log output (#114)
1 parent cd0095e commit 417cf78

File tree

3 files changed

+156
-12
lines changed

3 files changed

+156
-12
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ jobs:
3030
run: yarn lint
3131
- run: yarn build
3232
- run: yarn test
33+
env:
34+
CI: true
3335
timeout-minutes: 30

test/.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"rules": {
66
"no-var": "off",
77
"prefer-const": "off",
8-
"vars-on-top": "off"
8+
"vars-on-top": "off",
9+
"no-plusplus": "off"
910
}
1011
}

test/test.js

Lines changed: 152 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const path = require('path');
66
const pc = require('picocolors');
77
const { globSync } = require('tinyglobby');
88
const utils = require('./utils.js');
9+
const { spawn } = require('child_process');
910
const host = 'node' + utils.getNodeMajorVersion();
1011
let target = process.argv[2] || 'host';
1112
if (target === 'host') target = host;
@@ -17,6 +18,8 @@ if (target === 'host') target = host;
1718

1819
const flavor = process.env.FLAVOR || process.argv[3] || 'all';
1920

21+
const isCI = process.env.CI === 'true';
22+
2023
console.log('');
2124
console.log('*************************************');
2225
console.log(target + ' ' + flavor);
@@ -86,18 +89,156 @@ if (flavor.match(/^test/)) {
8689

8790
const files = globSync(list, { ignore });
8891

89-
files.sort().some(function (file) {
90-
file = path.resolve(file);
91-
try {
92-
utils.spawn.sync('node', [path.basename(file), target], {
92+
function msToHumanDuration(ms) {
93+
if (ms < 1000) return `${ms}ms`;
94+
const seconds = Math.floor(ms / 1000);
95+
const minutes = Math.floor(seconds / 60);
96+
const hours = Math.floor(minutes / 60);
97+
const human = [];
98+
if (hours > 0) human.push(`${hours}h`);
99+
if (minutes > 0) human.push(`${minutes % 60}m`);
100+
if (seconds > 0) human.push(`${seconds % 60}s`);
101+
return human.join(' ');
102+
}
103+
104+
/** @type {Array<import('child_process').ChildProcessWithoutNullStreams>} */
105+
const activeProcesses = [];
106+
107+
function runTest(file) {
108+
return new Promise((resolve, reject) => {
109+
const process = spawn('node', [path.basename(file), target], {
93110
cwd: path.dirname(file),
94-
stdio: 'inherit',
111+
stdio: 'pipe',
112+
});
113+
114+
activeProcesses.push(process);
115+
116+
const removeProcess = () => {
117+
const index = activeProcesses.indexOf(process);
118+
if (index !== -1) {
119+
activeProcesses.splice(index, 1);
120+
}
121+
};
122+
123+
const output = [];
124+
125+
const rejectWithError = (error) => {
126+
error.logOutput = `${error.message}\n${output.join('')}`;
127+
reject(error);
128+
};
129+
130+
process.on('close', (code) => {
131+
removeProcess();
132+
if (code !== 0) {
133+
rejectWithError(new Error(`Process exited with code ${code}`));
134+
} else {
135+
resolve();
136+
}
95137
});
96-
} catch (error) {
97-
console.log();
98-
console.log(`> ${pc.red('Error!')} ${error.message}`);
99-
console.log(`> ${pc.red('Error!')} ${file} FAILED (in ${target})`);
138+
139+
process.stdout.on('data', (data) => {
140+
output.push(data.toString());
141+
});
142+
143+
process.stderr.on('data', (data) => {
144+
output.push(data.toString());
145+
});
146+
147+
process.on('error', (error) => {
148+
removeProcess();
149+
rejectWithError(error);
150+
});
151+
});
152+
}
153+
154+
const clearLastLine = () => {
155+
if (isCI) return;
156+
process.stdout.moveCursor(0, -1); // up one line
157+
process.stdout.clearLine(1); // from cursor to end
158+
};
159+
160+
async function run() {
161+
let done = 0;
162+
let ok = 0;
163+
let failed = [];
164+
const start = Date.now();
165+
166+
function addLog(log, isError = false) {
167+
clearLastLine();
168+
if (isError) {
169+
console.error(log);
170+
} else {
171+
console.log(log);
172+
}
173+
}
174+
175+
const promises = files.sort().map((file) => async () => {
176+
file = path.resolve(file);
177+
const startTest = Date.now();
178+
try {
179+
if (!isCI) {
180+
console.log(pc.gray(`⏳ ${file} - ${done}/${files.length}`));
181+
}
182+
await runTest(file);
183+
ok++;
184+
addLog(
185+
pc.green(
186+
`✔ ${file} ok - ${msToHumanDuration(Date.now() - startTest)}`,
187+
),
188+
);
189+
} catch (error) {
190+
failed.push({
191+
file,
192+
error: error.message,
193+
output: error.logOutput,
194+
});
195+
addLog(
196+
pc.red(
197+
`✖ ${file} FAILED (in ${target}) - ${msToHumanDuration(Date.now() - startTest)}\n${error.message}`,
198+
),
199+
true,
200+
);
201+
}
202+
203+
done++;
204+
});
205+
206+
for (let i = 0; i < promises.length; i++) {
207+
await promises[i]();
208+
}
209+
210+
const end = Date.now();
211+
212+
console.log('');
213+
console.log('*************************************');
214+
console.log('Summary');
215+
console.log('*************************************');
216+
console.log('');
217+
218+
console.log(`Total: ${done}`);
219+
console.log(`Ok: ${ok}`);
220+
console.log(`Failed: ${failed.length}`);
221+
// print failed tests
222+
for (const { file, error, output } of failed) {
223+
console.log('');
224+
console.log(`--- ${file} ---`);
225+
console.log(pc.red(error));
226+
console.log(pc.red(output));
227+
}
228+
console.log(`Time: ${msToHumanDuration(end - start)}`);
229+
230+
if (failed.length > 0) {
100231
process.exit(2);
101232
}
102-
console.log(file, 'ok');
103-
});
233+
}
234+
235+
function cleanup() {
236+
for (const process of activeProcesses) {
237+
process.kill();
238+
}
239+
}
240+
241+
process.on('SIGINT', cleanup);
242+
process.on('SIGTERM', cleanup);
243+
244+
run();

0 commit comments

Comments
 (0)