feat(logger): Improve logging output and add --logfile support for persistent logs

This commit is contained in:
Philipp Kunz 2025-05-15 17:50:25 +00:00
parent dc0f859fad
commit 56f0f0be16
8 changed files with 137 additions and 221 deletions

View File

@ -1,5 +1,15 @@
# Changelog
## 2025-05-15 - 1.3.0 - feat(logger)
Improve logging output and add --logfile support for persistent logs
- Add new .claude/settings.local.json with logging permissions configuration
- Remove obsolete readme.plan.md
- Introduce test/test.console.ts to capture and display console outputs during tests
- Update CLI in ts/index.ts to replace '--log-file' with '--logfile' flag
- Enhance TsTestLogger to support file logging, clean ANSI sequences, and improved JSON output
- Forward TAP protocol logs to testConsoleOutput in TapParser for better console distinction
## 2025-05-15 - 1.2.0 - feat(logging)
Improve logging output, CLI option parsing, and test report formatting.

View File

@ -1,199 +0,0 @@
# Plan for improving logging and output in tstest
!! FIRST: Reread /home/philkunz/.claude/CLAUDE.md to ensure following all guidelines !!
## Goal - ✅ MOSTLY COMPLETED
- ✅ Make test output cleaner and more visually appealing
- ✅ Add structured logging capabilities
- ✅ Support different verbosity levels
- ✅ Improve CI/CD compatibility
- ✅ Add progress indicators and timing summaries
## Current State - UPDATED
- ✅ Clean, modern visual design with Unicode characters
- ✅ Structured output format (JSON support)
- ✅ Multiple verbosity levels (quiet, normal, verbose)
- ✅ Real-time output with cleaner formatting
- ✅ Better error aggregation and display
- ✅ TAP protocol support integrated with new logger
## Completed Improvements
### 1. ✅ Created new TsTestLogger class
- ✅ Centralized logging with consistent formatting
- ✅ Support for different output modes (normal, quiet, verbose)
- ✅ Better visual hierarchy with modern Unicode characters
- ✅ Progress indicators for multiple test files
- ✅ Structured error collection and display
### 2. ✅ Updated visual design
- ✅ Replaced heavy separators with cleaner alternatives
- ✅ Used better emoji and Unicode characters
- ✅ Added indentation for hierarchical display
- ✅ Grouped related information visually
- ✅ Added color coding consistently
### 3. ✅ Added command-line options
- ✅ `--quiet` for minimal CI-friendly output
- ✅ `--verbose` for detailed debugging information
- ✅ `--no-color` for environments without color support
- ✅ `--json` for structured JSON output
- ⏳ `--log-file <path>` for persistent logging (TODO)
### 4. ✅ Improved progress feedback
- ⏳ Show progress bar for multiple files (TODO)
- ✅ Display current file being executed
- ✅ Show real-time test counts
- ⏳ Add ETA for long test suites (TODO)
### 5. ✅ Better error and summary display
- ✅ Collect all errors and display at end
- ✅ Show timing metrics and performance summary (in verbose mode)
- ✅ Highlight slowest tests (in verbose mode)
- ✅ Add test failure context
### 6. ✅ Browser console integration
- ✅ Clearly separate browser logs from test output
- ⏳ Add browser log filtering options (TODO)
- ✅ Format browser errors specially
## Implementation Steps - COMPLETED
### Phase 1: ✅ Core Logger Implementation
1. ✅ Created `tstest.logging.ts` with TsTestLogger class
2. ✅ Added LogOptions interface for configuration
3. ✅ Implemented basic formatting methods
4. ✅ Added progress and summary methods
### Phase 2: ✅ Integration
1. ✅ Updated CLI to accept new command-line options
2. ✅ Modified TsTest class to use new logger
3. ✅ Updated TapParser to use structured logging
4. ✅ Updated TapCombinator for better summaries
### Phase 3: ✅ Visual Improvements
1. ✅ Replaced all existing separators
2. ✅ Updated color scheme
3. ✅ Added emoji and Unicode characters
4. ✅ Implemented hierarchical output
### Phase 4: ✅ Advanced Features
1. ✅ Add JSON output format
2. ⏳ Implement file-based logging (TODO)
3. ✅ Add performance metrics collection
4. ✅ Create error aggregation system
### Phase 5: ✅ Browser Integration
1. ✅ Update browser console forwarding
2. ✅ Add browser log formatting
3. ✅ Implement browser-specific indicators
## Files modified
- ✅ `ts/tstest.logging.ts` - New logger implementation (created)
- ✅ `ts/index.ts` - Added CLI options parsing
- ✅ `ts/tstest.classes.tstest.ts` - Integrated new logger
- ✅ `ts/tstest.classes.tap.parser.ts` - Updated output formatting
- ✅ `ts/tstest.classes.tap.combinator.ts` - Improved summary display
- ❌ `ts/tstest.logprefixes.ts` - Still in use, can be deprecated later
- ❌ `package.json` - No new dependencies needed
## Success Criteria - ACHIEVED
- ✅ Cleaner, more readable test output
- ✅ Configurable verbosity levels
- ✅ Better CI/CD integration
- ✅ Improved error visibility
- ✅ Performance metrics available
- ✅ Consistent visual design
- ✅ Maintained backward compatibility
## Example Output Comparison
### Current Output
```
☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰
**TSTEST** FOUND 4 TESTFILE(S):
**TSTEST** test/test.ts
------------------------------------------------
=> Running test/test.ts in node.js runtime.
= = = = = = = = = = = = = = = = = = = = = = = =
```
### Actual Output (IMPLEMENTED)
```
🔍 Test Discovery
Mode: directory
Pattern: test
Found: 4 test file(s)
▶️ test/test.ts (1/4)
Runtime: node.js
✅ prepare test (0ms)
Summary: 1/1 PASSED
▶️ test/test.single.ts (2/4)
Runtime: node.js
✅ single file test execution (1ms)
Summary: 1/1 PASSED
📊 Test Summary
┌────────────────────────────────┐
│ Total Files: 4 │
│ Total Tests: 4 │
│ Passed: 4 │
│ Failed: 0 │
│ Duration: 5739ms │
└────────────────────────────────┘
ALL TESTS PASSED! 🎉
```
### Additional Features Implemented
1. **Quiet Mode**:
```
Found 1 tests
✅ single file test execution
Summary: 1/1 | 1210ms | PASSED
```
2. **JSON Mode**:
```json
{"event":"discovery","count":1,"pattern":"test/test.single.ts","executionMode":"file"}
{"event":"fileStart","filename":"test/test.single.ts","runtime":"node.js","index":1,"total":1}
{"event":"testResult","testName":"single file test execution","passed":true,"duration":0}
{"event":"summary","summary":{"totalFiles":1,"totalTests":1,"totalPassed":1,"totalFailed":0,"totalDuration":1223,"fileResults":[...]}}
```
3. **Error Display**:
```
❌ Failed Tests:
test/test.fail.ts
❌ This test should fail
SOME TESTS FAILED! ❌
```
## Summary of Implementation
The logging improvement plan has been successfully implemented with the following achievements:
1. **Created a new centralized TsTestLogger class** that handles all output formatting
2. **Added multiple output modes**: quiet, normal, verbose, and JSON
3. **Improved visual design** with modern Unicode characters and emojis
4. **Added CLI argument parsing** for all new options
5. **Integrated the logger throughout the codebase** (TsTest, TapParser, TapCombinator)
6. **Better error aggregation and display** with failed tests shown at the end
7. **Performance metrics** available in verbose mode
8. **Clean, hierarchical output** that's much more readable
### Remaining TODOs
Only a few minor features remain unimplemented:
- File-based logging (--log-file option)
- Progress bar visualization
- ETA for long test suites
- Browser log filtering options
The core logging improvements are complete and provide a much better user experience!

21
test-output.log Normal file
View File

@ -0,0 +1,21 @@
🔍 Test Discovery
Mode: file
Pattern: test/test.single.ts
Found: 1 test file(s)
▶️ test/test.single.ts (1/1)
Runtime: node.js
✅ single file test execution (1ms)
Summary: 1/1 PASSED
📊 Test Summary
┌────────────────────────────────┐
│ Total Files: 1 │
│ Total Tests: 1 │
│ Passed: 1 │
│ Failed: 0 │
│ Duration: 1230ms │
└────────────────────────────────┘
ALL TESTS PASSED! 🎉

11
test/test.console.ts Normal file
View File

@ -0,0 +1,11 @@
import { expect, tap } from '@push.rocks/tapbundle';
tap.test('Test with console output', async () => {
console.log('Log message 1 from test');
console.log('Log message 2 from test');
console.error('Error message from test');
console.warn('Warning message from test');
expect(true).toBeTrue();
});
tap.start();

View File

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

View File

@ -33,9 +33,8 @@ export const runCli = async () => {
logOptions.json = true;
break;
case '--log-file':
if (i + 1 < args.length) {
logOptions.logFile = args[++i];
}
case '--logfile':
logOptions.logFile = true; // Set this as a flag, not a value
break;
default:
if (!arg.startsWith('-')) {
@ -52,7 +51,7 @@ export const runCli = async () => {
console.error(' --verbose, -v Verbose output');
console.error(' --no-color Disable colored output');
console.error(' --json Output results as JSON');
console.error(' --log-file Write logs to file');
console.error(' --logfile Write logs to .nogit/testlogs/[testfile].log');
process.exit(1);
}

View File

@ -102,7 +102,8 @@ export class TapParser {
this.activeTapTestResult.addLogLine(logLine);
}
if (this.logger) {
this.logger.tapOutput(logLine);
// This is console output from the test file, not TAP protocol
this.logger.testConsoleOutput(logLine);
}
}

View File

@ -1,12 +1,14 @@
import { coloredString as cs } from '@push.rocks/consolecolor';
import * as plugins from './tstest.plugins.js';
import * as fs from 'fs';
import * as path from 'path';
export interface LogOptions {
quiet?: boolean;
verbose?: boolean;
noColor?: boolean;
json?: boolean;
logFile?: string;
logFile?: boolean;
}
export interface TestFileResult {
@ -37,6 +39,7 @@ export class TsTestLogger {
private startTime: number;
private fileResults: TestFileResult[] = [];
private currentFileResult: TestFileResult | null = null;
private currentTestLogFile: string | null = null;
constructor(options: LogOptions = {}) {
this.options = options;
@ -51,11 +54,44 @@ export class TsTestLogger {
}
private log(message: string) {
if (this.options.json) return;
if (this.options.json) {
// For JSON mode, skip console output
// JSON output is handled by logJson method
return;
}
console.log(message);
if (this.options.logFile) {
// TODO: Implement file logging
// Log to the current test file log if we're in a test and --logfile is specified
if (this.currentTestLogFile) {
this.logToTestFile(message);
}
}
private logToFile(message: string) {
// This method is no longer used since we use logToTestFile for individual test logs
// Keeping it for potential future use with a global log file
}
private logToTestFile(message: string) {
try {
// Remove ANSI color codes for file logging
const cleanMessage = message.replace(/\u001b\[[0-9;]*m/g, '');
// Append to test log file
fs.appendFileSync(this.currentTestLogFile, cleanMessage + '\n');
} catch (error) {
// Silently fail to avoid disrupting the test run
}
}
private logJson(data: any) {
const jsonString = JSON.stringify(data);
console.log(jsonString);
// Also log to test file if --logfile is specified
if (this.currentTestLogFile) {
this.logToTestFile(jsonString);
}
}
@ -84,7 +120,7 @@ export class TsTestLogger {
// Test discovery
testDiscovery(count: number, pattern: string, executionMode: string) {
if (this.options.json) {
console.log(JSON.stringify({ event: 'discovery', count, pattern, executionMode }));
this.logJson({ event: 'discovery', count, pattern, executionMode });
return;
}
@ -109,8 +145,23 @@ export class TsTestLogger {
tests: []
};
// Only set up test log file if --logfile option is specified
if (this.options.logFile) {
const baseFilename = path.basename(filename, '.ts');
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${baseFilename}.log`);
// Ensure the directory exists
const logDir = path.dirname(this.currentTestLogFile);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
// Clear the log file for this test
fs.writeFileSync(this.currentTestLogFile, '');
}
if (this.options.json) {
console.log(JSON.stringify({ event: 'fileStart', filename, runtime, index, total }));
this.logJson({ event: 'fileStart', filename, runtime, index, total });
return;
}
@ -133,7 +184,7 @@ export class TsTestLogger {
}
if (this.options.json) {
console.log(JSON.stringify({ event: 'testResult', testName, passed, duration, error }));
this.logJson({ event: 'testResult', testName, passed, duration, error });
return;
}
@ -157,7 +208,7 @@ export class TsTestLogger {
}
if (this.options.json) {
console.log(JSON.stringify({ event: 'fileEnd', passed, failed, duration }));
this.logJson({ event: 'fileEnd', passed, failed, duration });
return;
}
@ -167,23 +218,45 @@ export class TsTestLogger {
const color = failed === 0 ? 'green' : 'red';
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
}
// Clear the current test log file reference only if using --logfile
if (this.options.logFile) {
this.currentTestLogFile = null;
}
}
// TAP output forwarding
// TAP output forwarding (for TAP protocol messages)
tapOutput(message: string, isError: boolean = false) {
if (this.options.json) return;
if (this.options.verbose || isError) {
const prefix = isError ? ' ⚠️ ' : ' ';
const color = isError ? 'red' : 'dim';
this.log(this.format(`${prefix}${message}`, color));
// Never show raw TAP protocol messages in console
// They are already processed by TapParser and shown in our format
// Always log to test file if --logfile is specified
if (this.currentTestLogFile) {
this.logToTestFile(` ${message}`);
}
}
// Console output from test files (non-TAP output)
testConsoleOutput(message: string) {
if (this.options.json) return;
// Show console output from test files only in verbose mode
if (this.options.verbose) {
this.log(this.format(` ${message}`, 'dim'));
}
// Always log to test file if --logfile is specified
if (this.currentTestLogFile) {
this.logToTestFile(` ${message}`);
}
}
// Browser console
browserConsole(message: string, level: string = 'log') {
if (this.options.json) {
console.log(JSON.stringify({ event: 'browserConsole', message, level }));
this.logJson({ event: 'browserConsole', message, level });
return;
}
@ -207,7 +280,7 @@ export class TsTestLogger {
};
if (this.options.json) {
console.log(JSON.stringify({ event: 'summary', summary }));
this.logJson({ event: 'summary', summary });
return;
}
@ -266,7 +339,7 @@ export class TsTestLogger {
// Error display
error(message: string, file?: string, stack?: string) {
if (this.options.json) {
console.log(JSON.stringify({ event: 'error', message, file, stack }));
this.logJson({ event: 'error', message, file, stack });
return;
}