Compare commits

..

4 Commits

6 changed files with 191 additions and 34 deletions

View File

@@ -1,5 +1,21 @@
# 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

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tstest",
"version": "1.10.0",
"version": "1.10.2",
"private": false,
"description": "a test utility to run tests that match test/**/*.ts",
"exports": {

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tstest',
version: '1.10.0',
version: '1.10.2',
description: 'a test utility to run tests that match test/**/*.ts'
}

View File

@@ -10,6 +10,7 @@ import { TsTestLogger } from './tstest.logging.js';
export class TapCombinator {
tapParserStore: TapParser[] = [];
skippedFiles: string[] = [];
private logger: TsTestLogger;
constructor(logger: TsTestLogger) {
@@ -19,10 +20,14 @@ export class TapCombinator {
addTapParser(tapParserArg: TapParser) {
this.tapParserStore.push(tapParserArg);
}
addSkippedFile(filename: string) {
this.skippedFiles.push(filename);
}
evaluate() {
// Call the logger's summary method
this.logger.summary();
// Call the logger's summary method with skipped files
this.logger.summary(this.skippedFiles);
// Check for failures
let failGlobal = false;

View File

@@ -38,27 +38,15 @@ export class TsTest {
}
async run() {
const testGroups = await this.testDir.getTestFileGroups();
let allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
// Apply file range filtering if specified
if (this.startFromFile !== null || this.stopAtFile !== null) {
const startIndex = this.startFromFile ? this.startFromFile - 1 : 0; // Convert to 0-based index
const endIndex = this.stopAtFile ? this.stopAtFile : allFiles.length;
allFiles = allFiles.slice(startIndex, endIndex);
// Filter the serial and parallel groups based on remaining files
testGroups.serial = testGroups.serial.filter(file => allFiles.includes(file));
Object.keys(testGroups.parallelGroups).forEach(groupName => {
testGroups.parallelGroups[groupName] = testGroups.parallelGroups[groupName].filter(file => allFiles.includes(file));
// Remove empty groups
if (testGroups.parallelGroups[groupName].length === 0) {
delete testGroups.parallelGroups[groupName];
}
});
// Move previous log files if --logfile option is used
if (this.logger.options.logFile) {
await this.movePreviousLogFiles();
}
// Log test discovery
const testGroups = await this.testDir.getTestFileGroups();
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
// Log test discovery - always show full count
this.logger.testDiscovery(
allFiles.length,
this.testDir.testPath,
@@ -71,7 +59,7 @@ export class TsTest {
// Execute serial tests first
for (const fileNameArg of testGroups.serial) {
fileIndex++;
await this.runSingleTest(fileNameArg, fileIndex, allFiles.length, tapCombinator);
await this.runSingleTestOrSkip(fileNameArg, fileIndex, allFiles.length, tapCombinator);
}
// Execute parallel groups sequentially
@@ -85,7 +73,7 @@ export class TsTest {
// Run all tests in this group in parallel
const parallelPromises = groupFiles.map(async (fileNameArg) => {
fileIndex++;
return this.runSingleTest(fileNameArg, fileIndex, allFiles.length, tapCombinator);
return this.runSingleTestOrSkip(fileNameArg, fileIndex, allFiles.length, tapCombinator);
});
await Promise.all(parallelPromises);
@@ -96,6 +84,24 @@ export class TsTest {
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) {
switch (true) {
case process.env.CI && fileNameArg.includes('.nonci.'):
@@ -166,7 +172,7 @@ export class TsTest {
});
server.addRoute(
'/test',
new plugins.typedserver.servertools.Handler('GET', async (req, res) => {
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
res.type('.html');
res.write(`
<html>
@@ -201,7 +207,7 @@ export class TsTest {
// lets do the browser bit
await this.smartbrowserInstance.start();
const evaluation = await this.smartbrowserInstance.evaluateOnPage(
await this.smartbrowserInstance.evaluateOnPage(
`http://localhost:3007/test?bundleName=${bundleFileName}`,
async () => {
// lets enable real time comms
@@ -214,12 +220,12 @@ export class TsTest {
const originalError = console.error;
// Override console methods to capture the logs
console.log = (...args) => {
console.log = (...args: any[]) => {
logStore.push(args.join(' '));
ws.send(args.join(' '));
originalLog(...args);
};
console.error = (...args) => {
console.error = (...args: any[]) => {
logStore.push(args.join(' '));
ws.send(args.join(' '));
originalError(...args);
@@ -270,4 +276,40 @@ export class TsTest {
}
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;
}
}
}

View File

@@ -30,12 +30,14 @@ export interface TestSummary {
totalTests: number;
totalPassed: number;
totalFailed: number;
totalSkipped: number;
totalDuration: number;
fileResults: TestFileResult[];
skippedFiles: string[];
}
export class TsTestLogger {
private options: LogOptions;
public readonly options: LogOptions;
private startTime: number;
private fileResults: TestFileResult[] = [];
private currentFileResult: TestFileResult | null = null;
@@ -245,6 +247,36 @@ export class TsTestLogger {
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
if (this.options.logFile) {
this.currentTestLogFile = null;
@@ -252,7 +284,7 @@ export class TsTestLogger {
}
// TAP output forwarding (for TAP protocol messages)
tapOutput(message: string, isError: boolean = false) {
tapOutput(message: string, _isError: boolean = false) {
if (this.options.json) return;
// 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
browserConsole(message: string, level: string = 'log') {
if (this.options.json) {
@@ -317,15 +362,17 @@ export class TsTestLogger {
}
// Final summary
summary() {
summary(skippedFiles: string[] = []) {
const totalDuration = Date.now() - this.startTime;
const summary: TestSummary = {
totalFiles: this.fileResults.length,
totalFiles: this.fileResults.length + skippedFiles.length,
totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
totalSkipped: skippedFiles.length,
totalDuration,
fileResults: this.fileResults
fileResults: this.fileResults,
skippedFiles
};
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(`│ Passed: ${summary.totalPassed.toString().padStart(14)}`, '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('└────────────────────────────────┘', '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;
}
}