Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
b525754035 | |||
aa10fc4ab3 | |||
3eb8ef22e5 | |||
763dc89f59 | |||
e0d8ede450 | |||
27c950c1a1 |
24
changelog.md
24
changelog.md
@ -1,5 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-05-24 - 1.10.2 - fix(tstest-logging)
|
||||||
|
Improve log file handling with log rotation and diff reporting
|
||||||
|
|
||||||
|
- Add .claude/settings.local.json to configure allowed shell and web operations
|
||||||
|
- Introduce movePreviousLogFiles function to archive previous log files when --logfile is used
|
||||||
|
- Enhance logging to generate error copies and diff reports between current and previous logs
|
||||||
|
- Add type annotations for console overrides in browser evaluations for improved stability
|
||||||
|
|
||||||
|
## 2025-05-23 - 1.10.1 - fix(tstest)
|
||||||
|
Improve file range filtering and summary logging by skipping test files outside the specified range and reporting them in the final summary.
|
||||||
|
|
||||||
|
- Introduce runSingleTestOrSkip to check file index against startFrom/stopAt values.
|
||||||
|
- Log skipped files with appropriate messages and add them to the summary.
|
||||||
|
- Update the logger to include total skipped files in the test summary.
|
||||||
|
- Add permission settings in .claude/settings.local.json to support new operations.
|
||||||
|
|
||||||
|
## 2025-05-23 - 1.10.0 - feat(cli)
|
||||||
|
Add --startFrom and --stopAt options to filter test files by range
|
||||||
|
|
||||||
|
- Introduced CLI options --startFrom and --stopAt in ts/index.ts for selective test execution
|
||||||
|
- Added validation to ensure provided range values are positive and startFrom is not greater than stopAt
|
||||||
|
- Propagated file range filtering into test grouping in tstest.classes.tstest.ts, applying the range filter across serial and parallel groups
|
||||||
|
- Updated usage messages to include the new options
|
||||||
|
|
||||||
## 2025-05-23 - 1.9.4 - fix(docs)
|
## 2025-05-23 - 1.9.4 - fix(docs)
|
||||||
Update documentation and configuration for legal notices and CI permissions. This commit adds a new local settings file for tool permissions, refines the legal and trademark sections in the readme, and improves glob test files with clearer log messages.
|
Update documentation and configuration for legal notices and CI permissions. This commit adds a new local settings file for tool permissions, refines the legal and trademark sections in the readme, and improves glob test files with clearer log messages.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "1.9.4",
|
"version": "1.10.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "a test utility to run tests that match test/**/*.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '1.9.4',
|
version: '1.10.2',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'a test utility to run tests that match test/**/*.ts'
|
||||||
}
|
}
|
||||||
|
45
ts/index.ts
45
ts/index.ts
@ -13,6 +13,8 @@ export const runCli = async () => {
|
|||||||
const logOptions: LogOptions = {};
|
const logOptions: LogOptions = {};
|
||||||
let testPath: string | null = null;
|
let testPath: string | null = null;
|
||||||
let tags: string[] = [];
|
let tags: string[] = [];
|
||||||
|
let startFromFile: number | null = null;
|
||||||
|
let stopAtFile: number | null = null;
|
||||||
|
|
||||||
// Parse options
|
// Parse options
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
@ -42,6 +44,32 @@ export const runCli = async () => {
|
|||||||
tags = args[++i].split(',');
|
tags = args[++i].split(',');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case '--startFrom':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
const value = parseInt(args[++i], 10);
|
||||||
|
if (isNaN(value) || value < 1) {
|
||||||
|
console.error('Error: --startFrom must be a positive integer');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
startFromFile = value;
|
||||||
|
} else {
|
||||||
|
console.error('Error: --startFrom requires a number argument');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '--stopAt':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
const value = parseInt(args[++i], 10);
|
||||||
|
if (isNaN(value) || value < 1) {
|
||||||
|
console.error('Error: --stopAt must be a positive integer');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
stopAtFile = value;
|
||||||
|
} else {
|
||||||
|
console.error('Error: --stopAt requires a number argument');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (!arg.startsWith('-')) {
|
if (!arg.startsWith('-')) {
|
||||||
testPath = arg;
|
testPath = arg;
|
||||||
@ -49,6 +77,12 @@ export const runCli = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate test file range options
|
||||||
|
if (startFromFile !== null && stopAtFile !== null && startFromFile > stopAtFile) {
|
||||||
|
console.error('Error: --startFrom cannot be greater than --stopAt');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!testPath) {
|
if (!testPath) {
|
||||||
console.error('You must specify a test directory/file/pattern as argument. Please try again.');
|
console.error('You must specify a test directory/file/pattern as argument. Please try again.');
|
||||||
console.error('\nUsage: tstest <path> [options]');
|
console.error('\nUsage: tstest <path> [options]');
|
||||||
@ -58,7 +92,9 @@ export const runCli = async () => {
|
|||||||
console.error(' --no-color Disable colored output');
|
console.error(' --no-color Disable colored output');
|
||||||
console.error(' --json Output results as JSON');
|
console.error(' --json Output results as JSON');
|
||||||
console.error(' --logfile Write logs to .nogit/testlogs/[testfile].log');
|
console.error(' --logfile Write logs to .nogit/testlogs/[testfile].log');
|
||||||
console.error(' --tags Run only tests with specified tags (comma-separated)');
|
console.error(' --tags <tags> Run only tests with specified tags (comma-separated)');
|
||||||
|
console.error(' --startFrom <n> Start running from test file number n');
|
||||||
|
console.error(' --stopAt <n> Stop running at test file number n');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +109,11 @@ export const runCli = async () => {
|
|||||||
executionMode = TestExecutionMode.DIRECTORY;
|
executionMode = TestExecutionMode.DIRECTORY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags);
|
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags, startFromFile, stopAtFile);
|
||||||
await tsTestInstance.run();
|
await tsTestInstance.run();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Execute CLI when this file is run directly
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
runCli();
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { TsTestLogger } from './tstest.logging.js';
|
|||||||
|
|
||||||
export class TapCombinator {
|
export class TapCombinator {
|
||||||
tapParserStore: TapParser[] = [];
|
tapParserStore: TapParser[] = [];
|
||||||
|
skippedFiles: string[] = [];
|
||||||
private logger: TsTestLogger;
|
private logger: TsTestLogger;
|
||||||
|
|
||||||
constructor(logger: TsTestLogger) {
|
constructor(logger: TsTestLogger) {
|
||||||
@ -20,9 +21,13 @@ export class TapCombinator {
|
|||||||
this.tapParserStore.push(tapParserArg);
|
this.tapParserStore.push(tapParserArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSkippedFile(filename: string) {
|
||||||
|
this.skippedFiles.push(filename);
|
||||||
|
}
|
||||||
|
|
||||||
evaluate() {
|
evaluate() {
|
||||||
// Call the logger's summary method
|
// Call the logger's summary method with skipped files
|
||||||
this.logger.summary();
|
this.logger.summary(this.skippedFiles);
|
||||||
|
|
||||||
// Check for failures
|
// Check for failures
|
||||||
let failGlobal = false;
|
let failGlobal = false;
|
||||||
|
@ -16,6 +16,8 @@ export class TsTest {
|
|||||||
public executionMode: TestExecutionMode;
|
public executionMode: TestExecutionMode;
|
||||||
public logger: TsTestLogger;
|
public logger: TsTestLogger;
|
||||||
public filterTags: string[];
|
public filterTags: string[];
|
||||||
|
public startFromFile: number | null;
|
||||||
|
public stopAtFile: number | null;
|
||||||
|
|
||||||
public smartshellInstance = new plugins.smartshell.Smartshell({
|
public smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
executor: 'bash',
|
executor: 'bash',
|
||||||
@ -26,18 +28,25 @@ export class TsTest {
|
|||||||
|
|
||||||
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
||||||
|
|
||||||
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = []) {
|
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null) {
|
||||||
this.executionMode = executionModeArg;
|
this.executionMode = executionModeArg;
|
||||||
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
||||||
this.logger = new TsTestLogger(logOptions);
|
this.logger = new TsTestLogger(logOptions);
|
||||||
this.filterTags = tags;
|
this.filterTags = tags;
|
||||||
|
this.startFromFile = startFromFile;
|
||||||
|
this.stopAtFile = stopAtFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
// Move previous log files if --logfile option is used
|
||||||
|
if (this.logger.options.logFile) {
|
||||||
|
await this.movePreviousLogFiles();
|
||||||
|
}
|
||||||
|
|
||||||
const testGroups = await this.testDir.getTestFileGroups();
|
const testGroups = await this.testDir.getTestFileGroups();
|
||||||
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
|
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
|
||||||
|
|
||||||
// Log test discovery
|
// Log test discovery - always show full count
|
||||||
this.logger.testDiscovery(
|
this.logger.testDiscovery(
|
||||||
allFiles.length,
|
allFiles.length,
|
||||||
this.testDir.testPath,
|
this.testDir.testPath,
|
||||||
@ -50,7 +59,7 @@ export class TsTest {
|
|||||||
// Execute serial tests first
|
// Execute serial tests first
|
||||||
for (const fileNameArg of testGroups.serial) {
|
for (const fileNameArg of testGroups.serial) {
|
||||||
fileIndex++;
|
fileIndex++;
|
||||||
await this.runSingleTest(fileNameArg, fileIndex, allFiles.length, tapCombinator);
|
await this.runSingleTestOrSkip(fileNameArg, fileIndex, allFiles.length, tapCombinator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute parallel groups sequentially
|
// Execute parallel groups sequentially
|
||||||
@ -64,7 +73,7 @@ export class TsTest {
|
|||||||
// Run all tests in this group in parallel
|
// Run all tests in this group in parallel
|
||||||
const parallelPromises = groupFiles.map(async (fileNameArg) => {
|
const parallelPromises = groupFiles.map(async (fileNameArg) => {
|
||||||
fileIndex++;
|
fileIndex++;
|
||||||
return this.runSingleTest(fileNameArg, fileIndex, allFiles.length, tapCombinator);
|
return this.runSingleTestOrSkip(fileNameArg, fileIndex, allFiles.length, tapCombinator);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(parallelPromises);
|
await Promise.all(parallelPromises);
|
||||||
@ -75,6 +84,24 @@ export class TsTest {
|
|||||||
tapCombinator.evaluate();
|
tapCombinator.evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async runSingleTestOrSkip(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
||||||
|
// Check if this file should be skipped based on range
|
||||||
|
if (this.startFromFile !== null && fileIndex < this.startFromFile) {
|
||||||
|
this.logger.testFileSkipped(fileNameArg, fileIndex, totalFiles, `before start range (${this.startFromFile})`);
|
||||||
|
tapCombinator.addSkippedFile(fileNameArg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.stopAtFile !== null && fileIndex > this.stopAtFile) {
|
||||||
|
this.logger.testFileSkipped(fileNameArg, fileIndex, totalFiles, `after stop range (${this.stopAtFile})`);
|
||||||
|
tapCombinator.addSkippedFile(fileNameArg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File is in range, run it
|
||||||
|
await this.runSingleTest(fileNameArg, fileIndex, totalFiles, tapCombinator);
|
||||||
|
}
|
||||||
|
|
||||||
private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case process.env.CI && fileNameArg.includes('.nonci.'):
|
case process.env.CI && fileNameArg.includes('.nonci.'):
|
||||||
@ -145,7 +172,7 @@ export class TsTest {
|
|||||||
});
|
});
|
||||||
server.addRoute(
|
server.addRoute(
|
||||||
'/test',
|
'/test',
|
||||||
new plugins.typedserver.servertools.Handler('GET', async (req, res) => {
|
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
|
||||||
res.type('.html');
|
res.type('.html');
|
||||||
res.write(`
|
res.write(`
|
||||||
<html>
|
<html>
|
||||||
@ -180,7 +207,7 @@ export class TsTest {
|
|||||||
|
|
||||||
// lets do the browser bit
|
// lets do the browser bit
|
||||||
await this.smartbrowserInstance.start();
|
await this.smartbrowserInstance.start();
|
||||||
const evaluation = await this.smartbrowserInstance.evaluateOnPage(
|
await this.smartbrowserInstance.evaluateOnPage(
|
||||||
`http://localhost:3007/test?bundleName=${bundleFileName}`,
|
`http://localhost:3007/test?bundleName=${bundleFileName}`,
|
||||||
async () => {
|
async () => {
|
||||||
// lets enable real time comms
|
// lets enable real time comms
|
||||||
@ -193,12 +220,12 @@ export class TsTest {
|
|||||||
const originalError = console.error;
|
const originalError = console.error;
|
||||||
|
|
||||||
// Override console methods to capture the logs
|
// Override console methods to capture the logs
|
||||||
console.log = (...args) => {
|
console.log = (...args: any[]) => {
|
||||||
logStore.push(args.join(' '));
|
logStore.push(args.join(' '));
|
||||||
ws.send(args.join(' '));
|
ws.send(args.join(' '));
|
||||||
originalLog(...args);
|
originalLog(...args);
|
||||||
};
|
};
|
||||||
console.error = (...args) => {
|
console.error = (...args: any[]) => {
|
||||||
logStore.push(args.join(' '));
|
logStore.push(args.join(' '));
|
||||||
ws.send(args.join(' '));
|
ws.send(args.join(' '));
|
||||||
originalError(...args);
|
originalError(...args);
|
||||||
@ -249,4 +276,40 @@ export class TsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async runInDeno() {}
|
public async runInDeno() {}
|
||||||
|
|
||||||
|
private async movePreviousLogFiles() {
|
||||||
|
const logDir = plugins.path.join('.nogit', 'testlogs');
|
||||||
|
const previousDir = plugins.path.join('.nogit', 'testlogs', 'previous');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all files in log directory
|
||||||
|
const files = await plugins.smartfile.fs.listFileTree(logDir, '*.log');
|
||||||
|
if (files.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure previous directory exists
|
||||||
|
await plugins.smartfile.fs.ensureDir(previousDir);
|
||||||
|
|
||||||
|
// Move each file to previous directory
|
||||||
|
for (const file of files) {
|
||||||
|
const filename = plugins.path.basename(file);
|
||||||
|
const sourcePath = plugins.path.join(logDir, filename);
|
||||||
|
const destPath = plugins.path.join(previousDir, filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read file content and write to new location
|
||||||
|
const content = await plugins.smartfile.fs.toStringSync(sourcePath);
|
||||||
|
await plugins.smartfile.fs.toFs(content, destPath);
|
||||||
|
// Remove original file
|
||||||
|
await plugins.smartfile.fs.remove(sourcePath);
|
||||||
|
} catch (error) {
|
||||||
|
// Silently continue if a file can't be moved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Directory might not exist, which is fine
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,12 +30,14 @@ export interface TestSummary {
|
|||||||
totalTests: number;
|
totalTests: number;
|
||||||
totalPassed: number;
|
totalPassed: number;
|
||||||
totalFailed: number;
|
totalFailed: number;
|
||||||
|
totalSkipped: number;
|
||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
fileResults: TestFileResult[];
|
fileResults: TestFileResult[];
|
||||||
|
skippedFiles: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TsTestLogger {
|
export class TsTestLogger {
|
||||||
private options: LogOptions;
|
public readonly options: LogOptions;
|
||||||
private startTime: number;
|
private startTime: number;
|
||||||
private fileResults: TestFileResult[] = [];
|
private fileResults: TestFileResult[] = [];
|
||||||
private currentFileResult: TestFileResult | null = null;
|
private currentFileResult: TestFileResult | null = null;
|
||||||
@ -245,6 +247,36 @@ export class TsTestLogger {
|
|||||||
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If using --logfile, handle error copy and diff detection
|
||||||
|
if (this.options.logFile && this.currentTestLogFile) {
|
||||||
|
try {
|
||||||
|
const logContent = fs.readFileSync(this.currentTestLogFile, 'utf-8');
|
||||||
|
const logDir = path.dirname(this.currentTestLogFile);
|
||||||
|
const logBasename = path.basename(this.currentTestLogFile);
|
||||||
|
|
||||||
|
// Create error copy if there were failures
|
||||||
|
if (failed > 0) {
|
||||||
|
const errorLogPath = path.join(logDir, `00err_${logBasename}`);
|
||||||
|
fs.writeFileSync(errorLogPath, logContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for previous version and create diff if changed
|
||||||
|
const previousLogPath = path.join(logDir, 'previous', logBasename);
|
||||||
|
if (fs.existsSync(previousLogPath)) {
|
||||||
|
const previousContent = fs.readFileSync(previousLogPath, 'utf-8');
|
||||||
|
|
||||||
|
// Simple check if content differs
|
||||||
|
if (previousContent !== logContent) {
|
||||||
|
const diffLogPath = path.join(logDir, `00diff_${logBasename}`);
|
||||||
|
const diffContent = this.createDiff(previousContent, logContent, logBasename);
|
||||||
|
fs.writeFileSync(diffLogPath, diffContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail to avoid disrupting the test run
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the current test log file reference only if using --logfile
|
// Clear the current test log file reference only if using --logfile
|
||||||
if (this.options.logFile) {
|
if (this.options.logFile) {
|
||||||
this.currentTestLogFile = null;
|
this.currentTestLogFile = null;
|
||||||
@ -252,7 +284,7 @@ export class TsTestLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TAP output forwarding (for TAP protocol messages)
|
// TAP output forwarding (for TAP protocol messages)
|
||||||
tapOutput(message: string, isError: boolean = false) {
|
tapOutput(message: string, _isError: boolean = false) {
|
||||||
if (this.options.json) return;
|
if (this.options.json) return;
|
||||||
|
|
||||||
// Never show raw TAP protocol messages in console
|
// Never show raw TAP protocol messages in console
|
||||||
@ -282,6 +314,19 @@ export class TsTestLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skipped test file
|
||||||
|
testFileSkipped(filename: string, index: number, total: number, reason: string) {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'fileSkipped', filename, index, total, reason });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) return;
|
||||||
|
|
||||||
|
this.log(this.format(`\n⏭️ ${filename} (${index}/${total})`, 'yellow'));
|
||||||
|
this.log(this.format(` Skipped: ${reason}`, 'dim'));
|
||||||
|
}
|
||||||
|
|
||||||
// Browser console
|
// Browser console
|
||||||
browserConsole(message: string, level: string = 'log') {
|
browserConsole(message: string, level: string = 'log') {
|
||||||
if (this.options.json) {
|
if (this.options.json) {
|
||||||
@ -317,15 +362,17 @@ export class TsTestLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Final summary
|
// Final summary
|
||||||
summary() {
|
summary(skippedFiles: string[] = []) {
|
||||||
const totalDuration = Date.now() - this.startTime;
|
const totalDuration = Date.now() - this.startTime;
|
||||||
const summary: TestSummary = {
|
const summary: TestSummary = {
|
||||||
totalFiles: this.fileResults.length,
|
totalFiles: this.fileResults.length + skippedFiles.length,
|
||||||
totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
|
totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
|
||||||
totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
|
totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
|
||||||
totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
|
totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
|
||||||
|
totalSkipped: skippedFiles.length,
|
||||||
totalDuration,
|
totalDuration,
|
||||||
fileResults: this.fileResults
|
fileResults: this.fileResults,
|
||||||
|
skippedFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.options.json) {
|
if (this.options.json) {
|
||||||
@ -346,6 +393,9 @@ export class TsTestLogger {
|
|||||||
this.log(this.format(`│ Total Tests: ${summary.totalTests.toString().padStart(14)} │`, 'white'));
|
this.log(this.format(`│ Total Tests: ${summary.totalTests.toString().padStart(14)} │`, 'white'));
|
||||||
this.log(this.format(`│ Passed: ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
|
this.log(this.format(`│ Passed: ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
|
||||||
this.log(this.format(`│ Failed: ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
|
this.log(this.format(`│ Failed: ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
|
||||||
|
if (summary.totalSkipped > 0) {
|
||||||
|
this.log(this.format(`│ Skipped: ${summary.totalSkipped.toString().padStart(14)} │`, 'yellow'));
|
||||||
|
}
|
||||||
this.log(this.format(`│ Duration: ${totalDuration.toString().padStart(14)}ms │`, 'white'));
|
this.log(this.format(`│ Duration: ${totalDuration.toString().padStart(14)}ms │`, 'white'));
|
||||||
this.log(this.format('└────────────────────────────────┘', 'dim'));
|
this.log(this.format('└────────────────────────────────┘', 'dim'));
|
||||||
|
|
||||||
@ -404,4 +454,48 @@ export class TsTestLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a diff between two log contents
|
||||||
|
private createDiff(previousContent: string, currentContent: string, filename: string): string {
|
||||||
|
const previousLines = previousContent.split('\n');
|
||||||
|
const currentLines = currentContent.split('\n');
|
||||||
|
|
||||||
|
let diff = `DIFF REPORT: ${filename}\n`;
|
||||||
|
diff += `Generated: ${new Date().toISOString()}\n`;
|
||||||
|
diff += '='.repeat(80) + '\n\n';
|
||||||
|
|
||||||
|
// Simple line-by-line comparison
|
||||||
|
const maxLines = Math.max(previousLines.length, currentLines.length);
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLines; i++) {
|
||||||
|
const prevLine = previousLines[i] || '';
|
||||||
|
const currLine = currentLines[i] || '';
|
||||||
|
|
||||||
|
if (prevLine !== currLine) {
|
||||||
|
hasChanges = true;
|
||||||
|
if (i < previousLines.length && i >= currentLines.length) {
|
||||||
|
// Line was removed
|
||||||
|
diff += `- [Line ${i + 1}] ${prevLine}\n`;
|
||||||
|
} else if (i >= previousLines.length && i < currentLines.length) {
|
||||||
|
// Line was added
|
||||||
|
diff += `+ [Line ${i + 1}] ${currLine}\n`;
|
||||||
|
} else {
|
||||||
|
// Line was modified
|
||||||
|
diff += `- [Line ${i + 1}] ${prevLine}\n`;
|
||||||
|
diff += `+ [Line ${i + 1}] ${currLine}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChanges) {
|
||||||
|
diff += 'No changes detected.\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
diff += '\n' + '='.repeat(80) + '\n';
|
||||||
|
diff += `Previous version had ${previousLines.length} lines\n`;
|
||||||
|
diff += `Current version has ${currentLines.length} lines\n`;
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user