Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
c26145205f | |||
82fc22653b | |||
3d85f54be0 | |||
9464c17c15 | |||
91b99ce304 | |||
899045e6aa | |||
845f146e91 | |||
d1f8652fc7 | |||
f717078558 | |||
d2c0e533b5 | |||
d3c7fce595 | |||
570e2d6b3b | |||
b7f4b7b3b8 | |||
424046b0de | |||
0f762f2063 | |||
82757c4abc | |||
7aaeed0dc6 |
57
changelog.md
57
changelog.md
@ -1,5 +1,62 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-05-26 - 2.3.0 - feat(cli)
|
||||||
|
Add '--version' option and warn against global tstest usage in the tstest project
|
||||||
|
|
||||||
|
- Introduced a new '--version' CLI flag that prints the version from package.json
|
||||||
|
- Added logic in ts/index.ts to detect if tstest is run globally within its own project and issue a warning
|
||||||
|
- Added .claude/settings.local.json to configure allowed permissions for various commands
|
||||||
|
|
||||||
|
## 2025-05-26 - 2.2.6 - fix(tstest)
|
||||||
|
Improve timeout warning timer management and summary output formatting in the test runner.
|
||||||
|
|
||||||
|
- Removed the global timeoutWarningTimer and replaced it with local warning timers in runInNode and runInChrome methods.
|
||||||
|
- Added warnings when test files run for over one minute if no timeout is specified.
|
||||||
|
- Ensured proper clearing of warning timers on successful completion or timeout.
|
||||||
|
- Enhanced quiet mode summary output to clearly display passed and failed test counts.
|
||||||
|
|
||||||
|
## 2025-05-26 - 2.2.5 - fix(protocol)
|
||||||
|
Fix inline timing metadata parsing and enhance test coverage for performance metrics and timing edge cases
|
||||||
|
|
||||||
|
- Updated the protocol parser to correctly parse inline key:value pairs while excluding prefixed formats (META:, SKIP:, TODO:, EVENT:)
|
||||||
|
- Added new tests for performance metrics, timing edge cases, and protocol timing to verify accurate timing capture and retry handling
|
||||||
|
- Expanded documentation in readme.hints.md to detail the updated timing implementation and parser fixes
|
||||||
|
|
||||||
|
## 2025-05-26 - 2.2.4 - fix(logging)
|
||||||
|
Improve performance metrics reporting and add local permissions configuration
|
||||||
|
|
||||||
|
- Add .claude/settings.local.json to configure allowed permissions for various commands
|
||||||
|
- Update tstest logging: compute average test duration from actual durations and adjust slowest test display formatting
|
||||||
|
|
||||||
|
## 2025-05-26 - 2.2.3 - fix(readme/ts/tstest.plugins)
|
||||||
|
Update npm package scope and documentation to use '@git.zone' instead of '@gitzone', and add local settings configuration.
|
||||||
|
|
||||||
|
- Changed npm package links and source repository URLs in readme from '@gitzone/tstest' to '@git.zone/tstest'.
|
||||||
|
- Updated comments in ts/tstest.plugins.ts to reflect the correct '@git.zone' scope.
|
||||||
|
- Added .claude/settings.local.json file with local permission settings.
|
||||||
|
|
||||||
|
## 2025-05-26 - 2.2.2 - fix(config)
|
||||||
|
Cleanup project configuration by adding local CLAUDE settings and removing redundant license files
|
||||||
|
|
||||||
|
- Added .claude/settings.local.json with updated permissions for CLI and build tasks
|
||||||
|
- Removed license and license.md files to streamline repository content
|
||||||
|
|
||||||
|
## 2025-05-26 - 2.2.1 - fix(repo configuration)
|
||||||
|
Update repository metadata to use 'git.zone' naming and add local permission settings
|
||||||
|
|
||||||
|
- Changed githost from 'gitlab.com' to 'code.foss.global' and gitscope from 'gitzone' to 'git.zone' in npmextra.json
|
||||||
|
- Updated npm package name from '@gitzone/tstest' to '@git.zone/tstest' in npmextra.json and readme.md
|
||||||
|
- Added .claude/settings.local.json with new permission configuration
|
||||||
|
|
||||||
|
## 2025-05-26 - 2.2.0 - feat(watch mode)
|
||||||
|
Add watch mode support with CLI options and enhanced documentation
|
||||||
|
|
||||||
|
- Introduce '--watch' (or '-w') and '--watch-ignore' CLI flags for automatic test re-runs
|
||||||
|
- Integrate @push.rocks/smartchok for file watching with 300ms debouncing
|
||||||
|
- Update readme.md and readme.hints.md with detailed instructions and examples for watch mode
|
||||||
|
- Add a demo test file (test/watch-demo/test.demo.ts) to illustrate the new feature
|
||||||
|
- Add smartchok dependency in package.json
|
||||||
|
|
||||||
## 2025-05-26 - 2.1.0 - feat(core)
|
## 2025-05-26 - 2.1.0 - feat(core)
|
||||||
Implement Protocol V2 with enhanced settings and lifecycle hooks
|
Implement Protocol V2 with enhanced settings and lifecycle hooks
|
||||||
|
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
"gitzone": {
|
"gitzone": {
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "gitzone",
|
"gitscope": "git.zone",
|
||||||
"gitrepo": "tstest",
|
"gitrepo": "tstest",
|
||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "a test utility to run tests that match test/**/*.ts",
|
||||||
"npmPackagename": "@gitzone/tstest",
|
"npmPackagename": "@git.zone/tstest",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "2.1.0",
|
"version": "2.3.0",
|
||||||
"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": {
|
||||||
@ -34,6 +34,7 @@
|
|||||||
"@push.rocks/consolecolor": "^2.0.2",
|
"@push.rocks/consolecolor": "^2.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
"@push.rocks/qenv": "^6.1.0",
|
||||||
"@push.rocks/smartbrowser": "^2.0.8",
|
"@push.rocks/smartbrowser": "^2.0.8",
|
||||||
|
"@push.rocks/smartchok": "^1.0.34",
|
||||||
"@push.rocks/smartcrypto": "^2.0.4",
|
"@push.rocks/smartcrypto": "^2.0.4",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartenv": "^5.0.12",
|
"@push.rocks/smartenv": "^5.0.12",
|
||||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -26,6 +26,9 @@ importers:
|
|||||||
'@push.rocks/smartbrowser':
|
'@push.rocks/smartbrowser':
|
||||||
specifier: ^2.0.8
|
specifier: ^2.0.8
|
||||||
version: 2.0.8
|
version: 2.0.8
|
||||||
|
'@push.rocks/smartchok':
|
||||||
|
specifier: ^1.0.34
|
||||||
|
version: 1.0.34
|
||||||
'@push.rocks/smartcrypto':
|
'@push.rocks/smartcrypto':
|
||||||
specifier: ^2.0.4
|
specifier: ^2.0.4
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
|
107
readme.hints.md
107
readme.hints.md
@ -215,4 +215,109 @@ The Enhanced Communication system has been implemented to provide rich, real-tim
|
|||||||
- Events are transmitted via Protocol V2's `EVENT` block type
|
- Events are transmitted via Protocol V2's `EVENT` block type
|
||||||
- Event data is JSON-encoded within protocol markers
|
- Event data is JSON-encoded within protocol markers
|
||||||
- Parser handles events asynchronously for real-time updates
|
- Parser handles events asynchronously for real-time updates
|
||||||
- Visual diffs are generated using custom diff algorithms for each data type
|
- Visual diffs are generated using custom diff algorithms for each data type
|
||||||
|
|
||||||
|
## Watch Mode (Phase 4)
|
||||||
|
|
||||||
|
tstest now supports watch mode for automatic test re-runs on file changes.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
```bash
|
||||||
|
tstest test/**/*.ts --watch
|
||||||
|
tstest test/specific.ts -w
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Automatic Re-runs**: Tests re-run when any watched file changes
|
||||||
|
- **Debouncing**: Multiple rapid changes are batched (300ms delay)
|
||||||
|
- **Clear Output**: Console is cleared before each run for clean results
|
||||||
|
- **Status Updates**: Shows which files triggered the re-run
|
||||||
|
- **Graceful Exit**: Press Ctrl+C to stop watching
|
||||||
|
|
||||||
|
### Options
|
||||||
|
- `--watch` or `-w`: Enable watch mode
|
||||||
|
- `--watch-ignore`: Comma-separated patterns to ignore (e.g., `--watch-ignore node_modules,dist`)
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
- Uses `@push.rocks/smartchok` for cross-platform file watching
|
||||||
|
- Watches the entire project directory from where tests are run
|
||||||
|
- Ignores changes matching the ignore patterns
|
||||||
|
- Shows "Waiting for file changes..." between runs
|
||||||
|
|
||||||
|
## Fixed Issues
|
||||||
|
|
||||||
|
### tap.skip.test(), tap.todo(), and tap.only.test() (Fixed)
|
||||||
|
|
||||||
|
Previously reported issues with these methods have been resolved:
|
||||||
|
|
||||||
|
1. **tap.skip.test()** - Now properly creates test objects that are counted in test results
|
||||||
|
- Tests marked with `skip.test()` appear in the test count
|
||||||
|
- Shows as passed with skip directive in TAP output
|
||||||
|
- `markAsSkipped()` method added to handle pre-test skip marking
|
||||||
|
|
||||||
|
2. **tap.todo.test()** - Fully implemented with test object creation
|
||||||
|
- Supports both `tap.todo.test('description')` and `tap.todo.test('description', testFunc)`
|
||||||
|
- Todo tests are counted and marked with todo directive
|
||||||
|
- Both regular and parallel todo tests supported
|
||||||
|
|
||||||
|
3. **tap.only.test()** - Works correctly for focused testing
|
||||||
|
- When `.only` tests exist, only those tests run
|
||||||
|
- Other tests are not executed but still counted
|
||||||
|
- Both regular and parallel only tests supported
|
||||||
|
|
||||||
|
These fixes ensure accurate test counts and proper TAP-compliant output for all test states.
|
||||||
|
|
||||||
|
## Test Timing Implementation
|
||||||
|
|
||||||
|
### Timing Architecture
|
||||||
|
|
||||||
|
Test timing is captured using `@push.rocks/smarttime`'s `HrtMeasurement` class, which provides high-resolution timing:
|
||||||
|
|
||||||
|
1. **Timing Capture**:
|
||||||
|
- Each `TapTest` instance has its own `HrtMeasurement`
|
||||||
|
- Timer starts immediately before test function execution
|
||||||
|
- Timer stops after test completes (or fails/times out)
|
||||||
|
- Millisecond precision is used for reporting
|
||||||
|
|
||||||
|
2. **Protocol Integration**:
|
||||||
|
- Timing is embedded in TAP output using Protocol V2 markers
|
||||||
|
- Inline format for simple timing: `ok 1 - test name ⟦TSTEST:time:123⟧`
|
||||||
|
- Block format for complex metadata: `⟦TSTEST:META:{"time":456,"file":"test.ts"}⟧`
|
||||||
|
|
||||||
|
3. **Performance Metrics Calculation**:
|
||||||
|
- Average is calculated from sum of individual test times, not total runtime
|
||||||
|
- Slowest test detection prefers tests with >0ms duration
|
||||||
|
- Failed tests still contribute their execution time to metrics
|
||||||
|
|
||||||
|
### Edge Cases and Considerations
|
||||||
|
|
||||||
|
1. **Sub-millisecond Tests**:
|
||||||
|
- Very fast tests may report 0ms due to millisecond rounding
|
||||||
|
- Performance metrics handle this by showing "All tests completed in <1ms" when appropriate
|
||||||
|
|
||||||
|
2. **Special Test States**:
|
||||||
|
- **Skipped tests**: Report 0ms (not executed)
|
||||||
|
- **Todo tests**: Report 0ms (not executed)
|
||||||
|
- **Failed tests**: Report actual execution time before failure
|
||||||
|
- **Timeout tests**: Report time until timeout occurred
|
||||||
|
|
||||||
|
3. **Parallel Test Timing**:
|
||||||
|
- Each parallel test tracks its own execution time independently
|
||||||
|
- Parallel tests may have overlapping execution periods
|
||||||
|
- Total suite time reflects wall-clock time, not sum of test times
|
||||||
|
|
||||||
|
4. **Hook Timing**:
|
||||||
|
- `beforeEach`/`afterEach` hooks are not included in individual test times
|
||||||
|
- Only the actual test function execution is measured
|
||||||
|
|
||||||
|
5. **Retry Timing**:
|
||||||
|
- When tests retry, only the final attempt's duration is reported
|
||||||
|
- Each retry attempt emits separate `test:started` events
|
||||||
|
|
||||||
|
### Parser Fix for Timing Metadata
|
||||||
|
|
||||||
|
The protocol parser was fixed to correctly handle inline timing metadata:
|
||||||
|
- Changed condition from `!simpleMatch[1].includes(':')` to check for simple key:value pairs
|
||||||
|
- Excludes prefixed formats (META:, SKIP:, TODO:, EVENT:) while parsing simple formats like `time:250`
|
||||||
|
|
||||||
|
This ensures timing metadata is correctly extracted and displayed in test results.
|
147
readme.md
147
readme.md
@ -1,9 +1,9 @@
|
|||||||
# @gitzone/tstest
|
# @git.zone/tstest
|
||||||
🧪 **A powerful, modern test runner for TypeScript** - making your test runs beautiful and informative!
|
🧪 **A powerful, modern test runner for TypeScript** - making your test runs beautiful and informative!
|
||||||
|
|
||||||
## Availabililty and Links
|
## Availabililty and Links
|
||||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@gitzone/tstest)
|
* [npmjs.org (npm package)](https://www.npmjs.com/package/@git.zone/tstest)
|
||||||
* [code.foss.global (source)](https://code.foss.global/gitzone/tstest)
|
* [code.foss.global (source)](https://code.foss.global/git.zone/tstest)
|
||||||
|
|
||||||
## Why tstest?
|
## Why tstest?
|
||||||
|
|
||||||
@ -27,13 +27,19 @@
|
|||||||
- 🔁 **Retry Logic** - Automatically retry failing tests
|
- 🔁 **Retry Logic** - Automatically retry failing tests
|
||||||
- 🛠️ **Test Fixtures** - Create reusable test data
|
- 🛠️ **Test Fixtures** - Create reusable test data
|
||||||
- 📦 **Browser-Compatible** - Full browser support with embedded tapbundle
|
- 📦 **Browser-Compatible** - Full browser support with embedded tapbundle
|
||||||
|
- 👀 **Watch Mode** - Automatically re-run tests on file changes
|
||||||
|
- 📊 **Real-time Progress** - Live test execution progress updates
|
||||||
|
- 🎨 **Visual Diffs** - Beautiful side-by-side diffs for failed assertions
|
||||||
|
- 🔄 **Event-based Reporting** - Real-time test lifecycle events
|
||||||
|
- ⚙️ **Test Configuration** - Flexible test settings with .tstest.json files
|
||||||
|
- 🚀 **Protocol V2** - Enhanced TAP protocol with Unicode delimiters
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --save-dev @gitzone/tstest
|
npm install --save-dev @git.zone/tstest
|
||||||
# or with pnpm
|
# or with pnpm
|
||||||
pnpm add -D @gitzone/tstest
|
pnpm add -D @git.zone/tstest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -73,6 +79,9 @@ tstest "test/unit/*.ts"
|
|||||||
| `--timeout <seconds>` | Timeout test files after specified seconds |
|
| `--timeout <seconds>` | Timeout test files after specified seconds |
|
||||||
| `--startFrom <n>` | Start running from test file number n |
|
| `--startFrom <n>` | Start running from test file number n |
|
||||||
| `--stopAt <n>` | Stop running at test file number n |
|
| `--stopAt <n>` | Stop running at test file number n |
|
||||||
|
| `--watch`, `-w` | Watch for file changes and re-run tests |
|
||||||
|
| `--watch-ignore <patterns>` | Ignore patterns in watch mode (comma-separated) |
|
||||||
|
| `--only` | Run only tests marked with .only |
|
||||||
|
|
||||||
### Example Outputs
|
### Example Outputs
|
||||||
|
|
||||||
@ -203,9 +212,9 @@ tap.only.test('focus on this', async () => {
|
|||||||
// Only this test will run
|
// Only this test will run
|
||||||
});
|
});
|
||||||
|
|
||||||
// Todo test
|
// Todo test - creates actual test object marked as todo
|
||||||
tap.todo('implement later', async () => {
|
tap.todo.test('implement later', async () => {
|
||||||
// Marked as todo
|
// This test will be counted but marked as todo
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chaining modifiers
|
// Chaining modifiers
|
||||||
@ -558,6 +567,115 @@ tapWrap.tap.test('wrapped test', async () => {
|
|||||||
|
|
||||||
## Advanced Features
|
## Advanced Features
|
||||||
|
|
||||||
|
### Watch Mode
|
||||||
|
|
||||||
|
Automatically re-run tests when files change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Watch all files in the project
|
||||||
|
tstest test/ --watch
|
||||||
|
|
||||||
|
# Watch with custom ignore patterns
|
||||||
|
tstest test/ --watch --watch-ignore "dist/**,coverage/**"
|
||||||
|
|
||||||
|
# Short form
|
||||||
|
tstest test/ -w
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- 👀 Shows which files triggered the re-run
|
||||||
|
- ⏱️ 300ms debouncing to batch rapid changes
|
||||||
|
- 🔄 Clears console between runs for clean output
|
||||||
|
- 📁 Intelligently ignores common non-source files
|
||||||
|
|
||||||
|
### Real-time Test Progress
|
||||||
|
|
||||||
|
tstest provides real-time updates during test execution:
|
||||||
|
|
||||||
|
```
|
||||||
|
▶️ test/api.test.ts (1/4)
|
||||||
|
Runtime: node.js
|
||||||
|
⏳ Running: api endpoint validation...
|
||||||
|
✅ api endpoint validation (145ms)
|
||||||
|
⏳ Running: error handling...
|
||||||
|
✅ error handling (23ms)
|
||||||
|
Summary: 2/2 PASSED
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Diffs for Failed Assertions
|
||||||
|
|
||||||
|
When assertions fail, tstest shows beautiful side-by-side diffs:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ should return correct user data
|
||||||
|
|
||||||
|
String Diff:
|
||||||
|
- Expected
|
||||||
|
+ Received
|
||||||
|
|
||||||
|
- Hello World
|
||||||
|
+ Hello Universe
|
||||||
|
|
||||||
|
Object Diff:
|
||||||
|
{
|
||||||
|
name: "John",
|
||||||
|
- age: 30,
|
||||||
|
+ age: 31,
|
||||||
|
email: "john@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Configuration (.tstest.json)
|
||||||
|
|
||||||
|
Configure test behavior with `.tstest.json` files:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timeout": 30000,
|
||||||
|
"retries": 2,
|
||||||
|
"bail": false,
|
||||||
|
"parallel": true,
|
||||||
|
"tags": ["unit", "fast"],
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration files are discovered in:
|
||||||
|
1. Test file directory
|
||||||
|
2. Parent directories (up to project root)
|
||||||
|
3. Project root
|
||||||
|
4. Home directory (`~/.tstest.json`)
|
||||||
|
|
||||||
|
Settings cascade and merge, with closer files taking precedence.
|
||||||
|
|
||||||
|
### Event-based Test Reporting
|
||||||
|
|
||||||
|
tstest emits detailed events during test execution for integration with CI/CD tools:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"event":"suite:started","file":"test/api.test.ts","timestamp":"2025-05-26T10:30:00.000Z"}
|
||||||
|
{"event":"test:started","name":"api endpoint validation","timestamp":"2025-05-26T10:30:00.100Z"}
|
||||||
|
{"event":"test:progress","name":"api endpoint validation","message":"Validating response schema"}
|
||||||
|
{"event":"test:completed","name":"api endpoint validation","passed":true,"duration":145}
|
||||||
|
{"event":"suite:completed","file":"test/api.test.ts","passed":true,"total":2,"failed":0}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enhanced TAP Protocol (Protocol V2)
|
||||||
|
|
||||||
|
tstest uses an enhanced TAP protocol with Unicode delimiters for better parsing:
|
||||||
|
|
||||||
|
```
|
||||||
|
⟦TSTEST:EVENT:test:started⟧{"name":"my test","timestamp":"2025-05-26T10:30:00.000Z"}
|
||||||
|
ok 1 my test
|
||||||
|
⟦TSTEST:EVENT:test:completed⟧{"name":"my test","passed":true,"duration":145}
|
||||||
|
```
|
||||||
|
|
||||||
|
This prevents conflicts with test output that might contain TAP-like formatting.
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
### Glob Pattern Support
|
### Glob Pattern Support
|
||||||
|
|
||||||
Run specific test patterns:
|
Run specific test patterns:
|
||||||
@ -731,6 +849,19 @@ tstest test/api/endpoints.test.ts --verbose --timeout 60
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### Version 1.11.0
|
||||||
|
- 👀 Added Watch Mode with `--watch`/`-w` flag for automatic test re-runs
|
||||||
|
- 📊 Implemented real-time test progress updates with event streaming
|
||||||
|
- 🎨 Added visual diffs for failed assertions with side-by-side comparison
|
||||||
|
- 🔄 Enhanced event-based test lifecycle reporting
|
||||||
|
- ⚙️ Added test configuration system with `.tstest.json` files
|
||||||
|
- 🚀 Implemented Protocol V2 with Unicode delimiters for better TAP parsing
|
||||||
|
- 🐛 Fixed `tap.todo()` to create proper test objects
|
||||||
|
- 🐛 Fixed `tap.skip.test()` to correctly create and count test objects
|
||||||
|
- 🐛 Fixed `tap.only.test()` implementation with `--only` flag support
|
||||||
|
- 📁 Added settings inheritance for cascading test configuration
|
||||||
|
- ⏱️ Added debouncing for file change events in watch mode
|
||||||
|
|
||||||
### Version 1.10.0
|
### Version 1.10.0
|
||||||
- ⏱️ Added `--timeout <seconds>` option for test file timeout protection
|
- ⏱️ Added `--timeout <seconds>` option for test file timeout protection
|
||||||
- 🎯 Added `--startFrom <n>` and `--stopAt <n>` options for test file range control
|
- 🎯 Added `--startFrom <n>` and `--stopAt <n>` options for test file range control
|
||||||
|
@ -149,11 +149,13 @@ tap.test('performance test', async (toolsArg) => {
|
|||||||
## 5. Test Execution Improvements
|
## 5. Test Execution Improvements
|
||||||
|
|
||||||
|
|
||||||
### 5.2 Watch Mode
|
### 5.2 Watch Mode ✅ COMPLETED
|
||||||
- Automatically re-run tests on file changes
|
- Automatically re-run tests on file changes
|
||||||
- Intelligent test selection based on changed files
|
- Debounced file change detection (300ms)
|
||||||
- Fast feedback loop for development
|
- Clear console output between runs
|
||||||
- Integration with IDE/editor plugins
|
- Shows which files triggered re-runs
|
||||||
|
- Graceful exit with Ctrl+C
|
||||||
|
- `--watch-ignore` option for excluding patterns
|
||||||
|
|
||||||
### 5.3 Advanced Test Filtering (Partial) ⚠️
|
### 5.3 Advanced Test Filtering (Partial) ⚠️
|
||||||
```typescript
|
```typescript
|
||||||
@ -304,14 +306,16 @@ tstest --changed
|
|||||||
- Trend analysis
|
- Trend analysis
|
||||||
- Flaky test detection
|
- Flaky test detection
|
||||||
|
|
||||||
### Known Issues to Fix
|
### Recently Fixed Issues ✅
|
||||||
- **tap.todo()**: Method exists but has no implementation
|
- **tap.todo()**: Now fully implemented with test object creation
|
||||||
- **tap.skip.test()**: Doesn't create test objects, just logs (breaks test count)
|
- **tap.skip.test()**: Now creates test objects and maintains accurate test count
|
||||||
|
- **tap.only.test()**: Works correctly - when .only tests exist, only those run
|
||||||
|
|
||||||
|
### Remaining Minor Issues
|
||||||
- **Protocol Output**: Some protocol messages still appear in console output
|
- **Protocol Output**: Some protocol messages still appear in console output
|
||||||
- **Only Tests**: `tap.only.test()` exists but `--only` mode not fully implemented
|
|
||||||
|
|
||||||
### Next Recommended Steps
|
### Next Recommended Steps
|
||||||
1. Add Watch Mode - high developer value
|
1. Add Watch Mode (Phase 4) - high developer value for fast feedback
|
||||||
2. Implement Custom Reporters - important for CI/CD integration
|
2. Implement Custom Reporters - important for CI/CD integration
|
||||||
3. Fix known issues: tap.todo() and tap.skip.test() implementations
|
3. Implement performance benchmarking API
|
||||||
4. Implement performance benchmarking API
|
4. Add better error messages with suggestions
|
@ -1,56 +0,0 @@
|
|||||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
|
||||||
|
|
||||||
tap.test('should show string diff', async () => {
|
|
||||||
const expected = `Hello World
|
|
||||||
This is a test
|
|
||||||
of multiline strings`;
|
|
||||||
|
|
||||||
const actual = `Hello World
|
|
||||||
This is a demo
|
|
||||||
of multiline strings`;
|
|
||||||
|
|
||||||
// This will fail and show a diff
|
|
||||||
expect(actual).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should show object diff', async () => {
|
|
||||||
const expected = {
|
|
||||||
name: 'John',
|
|
||||||
age: 30,
|
|
||||||
city: 'New York',
|
|
||||||
hobbies: ['reading', 'coding']
|
|
||||||
};
|
|
||||||
|
|
||||||
const actual = {
|
|
||||||
name: 'John',
|
|
||||||
age: 31,
|
|
||||||
city: 'Boston',
|
|
||||||
hobbies: ['reading', 'gaming']
|
|
||||||
};
|
|
||||||
|
|
||||||
// This will fail and show a diff
|
|
||||||
expect(actual).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should show array diff', async () => {
|
|
||||||
const expected = [1, 2, 3, 4, 5];
|
|
||||||
const actual = [1, 2, 3, 5, 6];
|
|
||||||
|
|
||||||
// This will fail and show a diff
|
|
||||||
expect(actual).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should show primitive diff', async () => {
|
|
||||||
const expected = 42;
|
|
||||||
const actual = 43;
|
|
||||||
|
|
||||||
// This will fail and show a diff
|
|
||||||
expect(actual).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should pass without diff', async () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
expect('hello').toEqual('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start({ throwOnError: false });
|
|
167
test/tapbundle/test.performance-metrics.ts
Normal file
167
test/tapbundle/test.performance-metrics.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
// Create tests with known, distinct timing patterns to verify metrics calculation
|
||||||
|
tap.test('metric test 1 - 10ms baseline', async (tools) => {
|
||||||
|
await tools.delayFor(10);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('metric test 2 - 20ms double baseline', async (tools) => {
|
||||||
|
await tools.delayFor(20);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('metric test 3 - 30ms triple baseline', async (tools) => {
|
||||||
|
await tools.delayFor(30);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('metric test 4 - 40ms quadruple baseline', async (tools) => {
|
||||||
|
await tools.delayFor(40);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('metric test 5 - 50ms quintuple baseline', async (tools) => {
|
||||||
|
await tools.delayFor(50);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that should be the slowest
|
||||||
|
tap.test('metric test slowest - 200ms intentionally slow', async (tools) => {
|
||||||
|
await tools.delayFor(200);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests to verify edge cases in average calculation
|
||||||
|
tap.test('metric test fast 1 - minimal work', async () => {
|
||||||
|
expect(1).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('metric test fast 2 - minimal work', async () => {
|
||||||
|
expect(2).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('metric test fast 3 - minimal work', async () => {
|
||||||
|
expect(3).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test to verify that failed tests still contribute to timing metrics
|
||||||
|
tap.test('metric test that fails - 60ms before failure', async (tools) => {
|
||||||
|
await tools.delayFor(60);
|
||||||
|
expect(true).toBeFalse(); // This will fail
|
||||||
|
});
|
||||||
|
|
||||||
|
// Describe block with timing to test aggregation
|
||||||
|
tap.describe('performance metrics in describe block', () => {
|
||||||
|
tap.test('described test 1 - 15ms', async (tools) => {
|
||||||
|
await tools.delayFor(15);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('described test 2 - 25ms', async (tools) => {
|
||||||
|
await tools.delayFor(25);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('described test 3 - 35ms', async (tools) => {
|
||||||
|
await tools.delayFor(35);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test timing with hooks
|
||||||
|
tap.describe('performance with hooks', () => {
|
||||||
|
let hookTime = 0;
|
||||||
|
|
||||||
|
tap.beforeEach(async () => {
|
||||||
|
// Hooks shouldn't count toward test time
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
hookTime += 10;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.afterEach(async () => {
|
||||||
|
// Hooks shouldn't count toward test time
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
hookTime += 10;
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with hooks 1 - should only count test time', async (tools) => {
|
||||||
|
await tools.delayFor(30);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
// Test time should be ~30ms, not 50ms (including hooks)
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with hooks 2 - should only count test time', async (tools) => {
|
||||||
|
await tools.delayFor(40);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
// Test time should be ~40ms, not 60ms (including hooks)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parallel tests to verify timing is captured correctly
|
||||||
|
tap.describe('parallel timing verification', () => {
|
||||||
|
const startTimes: Map<string, number> = new Map();
|
||||||
|
const endTimes: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
tap.testParallel('parallel metric 1 - 80ms', async (tools) => {
|
||||||
|
startTimes.set('p1', Date.now());
|
||||||
|
await tools.delayFor(80);
|
||||||
|
endTimes.set('p1', Date.now());
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.testParallel('parallel metric 2 - 90ms', async (tools) => {
|
||||||
|
startTimes.set('p2', Date.now());
|
||||||
|
await tools.delayFor(90);
|
||||||
|
endTimes.set('p2', Date.now());
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.testParallel('parallel metric 3 - 100ms', async (tools) => {
|
||||||
|
startTimes.set('p3', Date.now());
|
||||||
|
await tools.delayFor(100);
|
||||||
|
endTimes.set('p3', Date.now());
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('verify parallel execution', async () => {
|
||||||
|
// This test runs after parallel tests
|
||||||
|
// Verify they actually ran in parallel by checking overlapping times
|
||||||
|
if (startTimes.size === 3 && endTimes.size === 3) {
|
||||||
|
const p1Start = startTimes.get('p1')!;
|
||||||
|
const p2Start = startTimes.get('p2')!;
|
||||||
|
const p3Start = startTimes.get('p3')!;
|
||||||
|
const p1End = endTimes.get('p1')!;
|
||||||
|
const p2End = endTimes.get('p2')!;
|
||||||
|
const p3End = endTimes.get('p3')!;
|
||||||
|
|
||||||
|
// Start times should be very close (within 50ms)
|
||||||
|
expect(Math.abs(p1Start - p2Start)).toBeLessThan(50);
|
||||||
|
expect(Math.abs(p2Start - p3Start)).toBeLessThan(50);
|
||||||
|
|
||||||
|
// There should be overlap in execution
|
||||||
|
const p1Overlaps = p1Start < p2End && p1End > p2Start;
|
||||||
|
const p2Overlaps = p2Start < p3End && p2End > p3Start;
|
||||||
|
|
||||||
|
expect(p1Overlaps || p2Overlaps).toBeTrue();
|
||||||
|
} else {
|
||||||
|
// Skip verification if parallel tests didn't run yet
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test to ensure average calculation handles mixed timing correctly
|
||||||
|
tap.test('final metrics test - 5ms minimal', async (tools) => {
|
||||||
|
await tools.delayFor(5);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
|
||||||
|
console.log('\n📊 Expected Performance Metrics Summary:');
|
||||||
|
console.log('- Tests include a mix of durations from <1ms to 200ms');
|
||||||
|
console.log('- Slowest test should be "metric test slowest" at ~200ms');
|
||||||
|
console.log('- Average should be calculated from individual test times');
|
||||||
|
console.log('- Failed test should still contribute its 60ms to timing');
|
||||||
|
console.log('- Parallel tests should show their individual times (80ms, 90ms, 100ms)');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
@ -1,23 +0,0 @@
|
|||||||
import { tap, expect } from '../../ts_tapbundle/index.js';
|
|
||||||
|
|
||||||
tap.test('should show string diff', async () => {
|
|
||||||
const expected = `line 1
|
|
||||||
line 2
|
|
||||||
line 3`;
|
|
||||||
|
|
||||||
const actual = `line 1
|
|
||||||
line changed
|
|
||||||
line 3`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(actual).toEqual(expected);
|
|
||||||
} catch (e) {
|
|
||||||
// Expected to fail
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should pass', async () => {
|
|
||||||
expect('hello').toEqual('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start({ throwOnError: false });
|
|
214
test/tapbundle/test.timing-edge-cases.ts
Normal file
214
test/tapbundle/test.timing-edge-cases.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
tap.test('ultra-fast test - should capture sub-millisecond timing', async () => {
|
||||||
|
// This test does almost nothing, should complete in < 1ms
|
||||||
|
const x = 1 + 1;
|
||||||
|
expect(x).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with exact 1ms delay', async (tools) => {
|
||||||
|
const start = Date.now();
|
||||||
|
await tools.delayFor(1);
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
// Should be at least 1ms but could be more due to event loop
|
||||||
|
expect(elapsed).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with 10ms delay', async (tools) => {
|
||||||
|
await tools.delayFor(10);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with 100ms delay', async (tools) => {
|
||||||
|
await tools.delayFor(100);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with 250ms delay', async (tools) => {
|
||||||
|
await tools.delayFor(250);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with 500ms delay', async (tools) => {
|
||||||
|
await tools.delayFor(500);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with variable processing time', async (tools) => {
|
||||||
|
// Simulate variable processing
|
||||||
|
const iterations = 1000000;
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
sum += Math.sqrt(i);
|
||||||
|
}
|
||||||
|
expect(sum).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Add a small delay to ensure measurable time
|
||||||
|
await tools.delayFor(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with multiple async operations', async () => {
|
||||||
|
// Multiple promises in parallel
|
||||||
|
const results = await Promise.all([
|
||||||
|
new Promise(resolve => setTimeout(() => resolve(1), 10)),
|
||||||
|
new Promise(resolve => setTimeout(() => resolve(2), 20)),
|
||||||
|
new Promise(resolve => setTimeout(() => resolve(3), 30))
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(results).toEqual([1, 2, 3]);
|
||||||
|
// This should take at least 30ms (the longest delay)
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('test with synchronous heavy computation', async () => {
|
||||||
|
// Heavy synchronous computation
|
||||||
|
const fibonacci = (n: number): number => {
|
||||||
|
if (n <= 1) return n;
|
||||||
|
return fibonacci(n - 1) + fibonacci(n - 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate fibonacci(30) - should take measurable time
|
||||||
|
const result = fibonacci(30);
|
||||||
|
expect(result).toEqual(832040);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test with retry to see if timing accumulates correctly
|
||||||
|
tap.retry(2).test('test with retry - fails first then passes', async (tools) => {
|
||||||
|
// Get or initialize retry count
|
||||||
|
const retryCount = tools.context.get('retryCount') || 0;
|
||||||
|
tools.context.set('retryCount', retryCount + 1);
|
||||||
|
|
||||||
|
await tools.delayFor(50);
|
||||||
|
|
||||||
|
if (retryCount === 0) {
|
||||||
|
throw new Error('First attempt fails');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(retryCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test timeout handling
|
||||||
|
tap.timeout(100).test('test with timeout - should complete just in time', async (tools) => {
|
||||||
|
await tools.delayFor(80); // Just under the timeout
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip test - should show 0ms
|
||||||
|
tap.skip.test('skipped test - should report 0ms', async (tools) => {
|
||||||
|
await tools.delayFor(1000); // This won't execute
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Todo test - should show 0ms
|
||||||
|
tap.todo.test('todo test - should report 0ms', async (tools) => {
|
||||||
|
await tools.delayFor(1000); // This won't execute
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test with skip inside
|
||||||
|
tap.test('test that skips conditionally - should show time until skip', async (tools) => {
|
||||||
|
await tools.delayFor(25);
|
||||||
|
|
||||||
|
const shouldSkip = true;
|
||||||
|
if (shouldSkip) {
|
||||||
|
tools.skip('Skipping after 25ms');
|
||||||
|
}
|
||||||
|
|
||||||
|
// This won't execute
|
||||||
|
await tools.delayFor(1000);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test with very precise timing
|
||||||
|
tap.test('test with precise timing measurements', async (tools) => {
|
||||||
|
const measurements: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const start = process.hrtime.bigint();
|
||||||
|
await tools.delayFor(10);
|
||||||
|
const end = process.hrtime.bigint();
|
||||||
|
const durationMs = Number(end - start) / 1_000_000;
|
||||||
|
measurements.push(durationMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All measurements should be at least 10ms
|
||||||
|
measurements.forEach(m => {
|
||||||
|
expect(m).toBeGreaterThanOrEqual(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// But not too much more (accounting for timer precision)
|
||||||
|
measurements.forEach(m => {
|
||||||
|
expect(m).toBeLessThan(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that intentionally has 0 actual work
|
||||||
|
tap.test('empty test - absolute minimum execution time', async () => {
|
||||||
|
// Literally nothing
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test with promise that resolves immediately
|
||||||
|
tap.test('test with immediate promise resolution', async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test with microtask queue
|
||||||
|
tap.test('test with microtask queue processing', async () => {
|
||||||
|
let value = 0;
|
||||||
|
|
||||||
|
await Promise.resolve().then(() => {
|
||||||
|
value = 1;
|
||||||
|
return Promise.resolve();
|
||||||
|
}).then(() => {
|
||||||
|
value = 2;
|
||||||
|
return Promise.resolve();
|
||||||
|
}).then(() => {
|
||||||
|
value = 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(value).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test to verify timing accumulation in describe blocks
|
||||||
|
tap.describe('timing in describe blocks', () => {
|
||||||
|
let startTime: number;
|
||||||
|
|
||||||
|
tap.beforeEach(async () => {
|
||||||
|
startTime = Date.now();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.afterEach(async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('first test in describe', async (tools) => {
|
||||||
|
await tools.delayFor(10);
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
expect(elapsed).toBeGreaterThanOrEqual(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('second test in describe', async (tools) => {
|
||||||
|
await tools.delayFor(20);
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
expect(elapsed).toBeGreaterThanOrEqual(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parallel tests to see timing differences
|
||||||
|
tap.testParallel('parallel test 1 - 100ms', async (tools) => {
|
||||||
|
await tools.delayFor(100);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.testParallel('parallel test 2 - 50ms', async (tools) => {
|
||||||
|
await tools.delayFor(50);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.testParallel('parallel test 3 - 150ms', async (tools) => {
|
||||||
|
await tools.delayFor(150);
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
204
test/tapbundle/test.timing-protocol.ts
Normal file
204
test/tapbundle/test.timing-protocol.ts
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
import { ProtocolParser, ProtocolEmitter } from '../../ts_tapbundle_protocol/index.js';
|
||||||
|
|
||||||
|
// Test the protocol's ability to emit and parse timing metadata
|
||||||
|
tap.test('protocol should correctly emit timing metadata', async () => {
|
||||||
|
const emitter = new ProtocolEmitter();
|
||||||
|
|
||||||
|
const testResult = {
|
||||||
|
ok: true,
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'test with timing',
|
||||||
|
metadata: {
|
||||||
|
time: 123
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines = emitter.emitTest(testResult);
|
||||||
|
|
||||||
|
// Should have inline timing metadata
|
||||||
|
expect(lines.length).toEqual(1);
|
||||||
|
expect(lines[0]).toInclude('⟦TSTEST:time:123⟧');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('protocol should correctly parse timing metadata', async () => {
|
||||||
|
const parser = new ProtocolParser();
|
||||||
|
|
||||||
|
const line = 'ok 1 - test with timing ⟦TSTEST:time:456⟧';
|
||||||
|
const messages = parser.parseLine(line);
|
||||||
|
|
||||||
|
expect(messages.length).toEqual(1);
|
||||||
|
expect(messages[0].type).toEqual('test');
|
||||||
|
|
||||||
|
const content = messages[0].content as any;
|
||||||
|
expect(content.metadata).toBeDefined();
|
||||||
|
expect(content.metadata.time).toEqual(456);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('protocol should handle 0ms timing', async () => {
|
||||||
|
const parser = new ProtocolParser();
|
||||||
|
|
||||||
|
const line = 'ok 1 - ultra fast test ⟦TSTEST:time:0⟧';
|
||||||
|
const messages = parser.parseLine(line);
|
||||||
|
|
||||||
|
const content = messages[0].content as any;
|
||||||
|
expect(content.metadata.time).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('protocol should handle large timing values', async () => {
|
||||||
|
const parser = new ProtocolParser();
|
||||||
|
|
||||||
|
const line = 'ok 1 - slow test ⟦TSTEST:time:999999⟧';
|
||||||
|
const messages = parser.parseLine(line);
|
||||||
|
|
||||||
|
const content = messages[0].content as any;
|
||||||
|
expect(content.metadata.time).toEqual(999999);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('protocol should handle timing with other metadata', async () => {
|
||||||
|
const emitter = new ProtocolEmitter();
|
||||||
|
|
||||||
|
const testResult = {
|
||||||
|
ok: true,
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'complex test',
|
||||||
|
metadata: {
|
||||||
|
time: 789,
|
||||||
|
file: 'test.ts',
|
||||||
|
tags: ['slow', 'integration']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines = emitter.emitTest(testResult);
|
||||||
|
|
||||||
|
// Should use block metadata format for complex metadata
|
||||||
|
expect(lines.length).toBeGreaterThan(1);
|
||||||
|
expect(lines[1]).toInclude('META:');
|
||||||
|
expect(lines[1]).toInclude('"time":789');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('protocol should parse timing from block metadata', async () => {
|
||||||
|
const parser = new ProtocolParser();
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'ok 1 - complex test',
|
||||||
|
'⟦TSTEST:META:{"time":321,"file":"test.ts"}⟧'
|
||||||
|
];
|
||||||
|
|
||||||
|
let testResult: any;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const messages = parser.parseLine(line);
|
||||||
|
if (messages.length > 0 && messages[0].type === 'test') {
|
||||||
|
testResult = messages[0].content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(testResult).toBeDefined();
|
||||||
|
expect(testResult.metadata).toBeUndefined(); // Metadata comes separately in block format
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('timing for skipped tests should be 0 or missing', async () => {
|
||||||
|
const emitter = new ProtocolEmitter();
|
||||||
|
|
||||||
|
const testResult = {
|
||||||
|
ok: true,
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'skipped test',
|
||||||
|
directive: {
|
||||||
|
type: 'skip' as const,
|
||||||
|
reason: 'Not ready'
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
time: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines = emitter.emitTest(testResult);
|
||||||
|
expect(lines[0]).toInclude('# SKIP');
|
||||||
|
|
||||||
|
// If time is 0, it might be included or omitted
|
||||||
|
if (lines[0].includes('⟦TSTEST:')) {
|
||||||
|
expect(lines[0]).toInclude('time:0');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('protocol should handle fractional milliseconds', async () => {
|
||||||
|
const emitter = new ProtocolEmitter();
|
||||||
|
|
||||||
|
// Even though we use integers, test that protocol handles them correctly
|
||||||
|
const testResult = {
|
||||||
|
ok: true,
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'precise test',
|
||||||
|
metadata: {
|
||||||
|
time: 123 // Protocol uses integers for milliseconds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines = emitter.emitTest(testResult);
|
||||||
|
expect(lines[0]).toInclude('time:123');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('protocol should handle timing in retry scenarios', async () => {
|
||||||
|
const emitter = new ProtocolEmitter();
|
||||||
|
|
||||||
|
const testResult = {
|
||||||
|
ok: true,
|
||||||
|
testNumber: 1,
|
||||||
|
description: 'retry test',
|
||||||
|
metadata: {
|
||||||
|
time: 200,
|
||||||
|
retry: 2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines = emitter.emitTest(testResult);
|
||||||
|
// Should include both time and retry
|
||||||
|
expect(lines[0]).toMatch(/time:200.*retry:2|retry:2.*time:200/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test actual timing capture
|
||||||
|
tap.test('HrtMeasurement should capture accurate timing', async (tools) => {
|
||||||
|
// Import HrtMeasurement
|
||||||
|
const { HrtMeasurement } = await import('@push.rocks/smarttime');
|
||||||
|
|
||||||
|
const measurement = new HrtMeasurement();
|
||||||
|
measurement.start();
|
||||||
|
|
||||||
|
await tools.delayFor(50);
|
||||||
|
|
||||||
|
measurement.stop();
|
||||||
|
|
||||||
|
// Should be at least 50ms
|
||||||
|
expect(measurement.milliSeconds).toBeGreaterThanOrEqual(50);
|
||||||
|
// But not too much more (allow for some overhead)
|
||||||
|
expect(measurement.milliSeconds).toBeLessThan(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('multiple timing measurements should be independent', async (tools) => {
|
||||||
|
const { HrtMeasurement } = await import('@push.rocks/smarttime');
|
||||||
|
|
||||||
|
const measurement1 = new HrtMeasurement();
|
||||||
|
const measurement2 = new HrtMeasurement();
|
||||||
|
|
||||||
|
measurement1.start();
|
||||||
|
await tools.delayFor(25);
|
||||||
|
|
||||||
|
measurement2.start();
|
||||||
|
await tools.delayFor(25);
|
||||||
|
|
||||||
|
measurement1.stop();
|
||||||
|
await tools.delayFor(25);
|
||||||
|
measurement2.stop();
|
||||||
|
|
||||||
|
// measurement1 should be ~50ms (25ms + 25ms)
|
||||||
|
expect(measurement1.milliSeconds).toBeGreaterThanOrEqual(50);
|
||||||
|
expect(measurement1.milliSeconds).toBeLessThan(70);
|
||||||
|
|
||||||
|
// measurement2 should be ~50ms (25ms + 25ms)
|
||||||
|
expect(measurement2.milliSeconds).toBeGreaterThanOrEqual(50);
|
||||||
|
expect(measurement2.milliSeconds).toBeLessThan(70);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
17
test/watch-demo/test.demo.ts
Normal file
17
test/watch-demo/test.demo.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
|
||||||
|
// This test file demonstrates watch mode
|
||||||
|
// Try modifying this file while running: tstest test/watch-demo --watch
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
|
||||||
|
tap.test('demo test that changes', async () => {
|
||||||
|
expect(counter).toEqual(1);
|
||||||
|
console.log(`Test run at: ${new Date().toISOString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('another test', async () => {
|
||||||
|
expect('hello').toEqual('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '2.1.0',
|
version: '2.3.0',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'a test utility to run tests that match test/**/*.ts'
|
||||||
}
|
}
|
||||||
|
70
ts/index.ts
70
ts/index.ts
@ -8,6 +8,40 @@ export enum TestExecutionMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const runCli = async () => {
|
export const runCli = async () => {
|
||||||
|
// Check if we're using global tstest in the tstest project itself
|
||||||
|
try {
|
||||||
|
const packageJsonPath = `${process.cwd()}/package.json`;
|
||||||
|
const fs = await import('fs');
|
||||||
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
if (packageJson.name === '@git.zone/tstest') {
|
||||||
|
// Check if we're running from a global installation
|
||||||
|
const execPath = process.argv[1];
|
||||||
|
// Debug: log the paths (uncomment for debugging)
|
||||||
|
// console.log('DEBUG: Checking global tstest usage...');
|
||||||
|
// console.log('execPath:', execPath);
|
||||||
|
// console.log('cwd:', process.cwd());
|
||||||
|
// console.log('process.argv:', process.argv);
|
||||||
|
|
||||||
|
// Check if this is running from global installation
|
||||||
|
const isLocalCli = execPath.includes(process.cwd());
|
||||||
|
const isGlobalPnpm = process.argv.some(arg => arg.includes('.pnpm') && !arg.includes(process.cwd()));
|
||||||
|
const isGlobalNpm = process.argv.some(arg => arg.includes('npm/node_modules') && !arg.includes(process.cwd()));
|
||||||
|
|
||||||
|
if (!isLocalCli && (isGlobalPnpm || isGlobalNpm || !execPath.includes('node_modules'))) {
|
||||||
|
console.error('\n⚠️ WARNING: You are using a globally installed tstest in the tstest project itself!');
|
||||||
|
console.error(' This means you are NOT testing your local changes.');
|
||||||
|
console.error(' Please use one of these commands instead:');
|
||||||
|
console.error(' • node cli.js <test-path>');
|
||||||
|
console.error(' • pnpm test <test-path>');
|
||||||
|
console.error(' • ./cli.js <test-path> (if executable)\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently ignore any errors in this check
|
||||||
|
}
|
||||||
|
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const logOptions: LogOptions = {};
|
const logOptions: LogOptions = {};
|
||||||
@ -16,12 +50,26 @@ export const runCli = async () => {
|
|||||||
let startFromFile: number | null = null;
|
let startFromFile: number | null = null;
|
||||||
let stopAtFile: number | null = null;
|
let stopAtFile: number | null = null;
|
||||||
let timeoutSeconds: number | null = null;
|
let timeoutSeconds: number | null = null;
|
||||||
|
let watchMode: boolean = false;
|
||||||
|
let watchIgnorePatterns: string[] = [];
|
||||||
|
|
||||||
// Parse options
|
// Parse options
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
|
|
||||||
switch (arg) {
|
switch (arg) {
|
||||||
|
case '--version':
|
||||||
|
// Get version from package.json
|
||||||
|
try {
|
||||||
|
const fs = await import('fs');
|
||||||
|
const packagePath = new URL('../package.json', import.meta.url).pathname;
|
||||||
|
const packageData = JSON.parse(await fs.promises.readFile(packagePath, 'utf8'));
|
||||||
|
console.log(`tstest version ${packageData.version}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('tstest version unknown');
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
break;
|
||||||
case '--quiet':
|
case '--quiet':
|
||||||
case '-q':
|
case '-q':
|
||||||
logOptions.quiet = true;
|
logOptions.quiet = true;
|
||||||
@ -84,6 +132,18 @@ export const runCli = async () => {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case '--watch':
|
||||||
|
case '-w':
|
||||||
|
watchMode = true;
|
||||||
|
break;
|
||||||
|
case '--watch-ignore':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
watchIgnorePatterns = args[++i].split(',');
|
||||||
|
} else {
|
||||||
|
console.error('Error: --watch-ignore requires a comma-separated list of patterns');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (!arg.startsWith('-')) {
|
if (!arg.startsWith('-')) {
|
||||||
testPath = arg;
|
testPath = arg;
|
||||||
@ -101,6 +161,7 @@ export const runCli = async () => {
|
|||||||
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]');
|
||||||
console.error('\nOptions:');
|
console.error('\nOptions:');
|
||||||
|
console.error(' --version Show version information');
|
||||||
console.error(' --quiet, -q Minimal output');
|
console.error(' --quiet, -q Minimal output');
|
||||||
console.error(' --verbose, -v Verbose output');
|
console.error(' --verbose, -v Verbose output');
|
||||||
console.error(' --no-color Disable colored output');
|
console.error(' --no-color Disable colored output');
|
||||||
@ -110,6 +171,8 @@ export const runCli = async () => {
|
|||||||
console.error(' --startFrom <n> Start running from test file number n');
|
console.error(' --startFrom <n> Start running from test file number n');
|
||||||
console.error(' --stopAt <n> Stop running at test file number n');
|
console.error(' --stopAt <n> Stop running at test file number n');
|
||||||
console.error(' --timeout <s> Timeout test files after s seconds');
|
console.error(' --timeout <s> Timeout test files after s seconds');
|
||||||
|
console.error(' --watch, -w Watch for file changes and re-run tests');
|
||||||
|
console.error(' --watch-ignore Patterns to ignore in watch mode (comma-separated)');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +188,12 @@ export const runCli = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags, startFromFile, stopAtFile, timeoutSeconds);
|
const tsTestInstance = new TsTest(process.cwd(), testPath, executionMode, logOptions, tags, startFromFile, stopAtFile, timeoutSeconds);
|
||||||
await tsTestInstance.run();
|
|
||||||
|
if (watchMode) {
|
||||||
|
await tsTestInstance.runWatch(watchIgnorePatterns);
|
||||||
|
} else {
|
||||||
|
await tsTestInstance.run();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute CLI when this file is run directly
|
// Execute CLI when this file is run directly
|
||||||
|
@ -18,7 +18,6 @@ export class TsTest {
|
|||||||
public startFromFile: number | null;
|
public startFromFile: number | null;
|
||||||
public stopAtFile: number | null;
|
public stopAtFile: number | null;
|
||||||
public timeoutSeconds: number | null;
|
public timeoutSeconds: number | null;
|
||||||
private timeoutWarningTimer: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
public smartshellInstance = new plugins.smartshell.Smartshell({
|
public smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
executor: 'bash',
|
executor: 'bash',
|
||||||
@ -45,15 +44,6 @@ export class TsTest {
|
|||||||
await this.movePreviousLogFiles();
|
await this.movePreviousLogFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start timeout warning timer if no timeout was specified
|
|
||||||
if (this.timeoutSeconds === null) {
|
|
||||||
this.timeoutWarningTimer = setTimeout(() => {
|
|
||||||
this.logger.warning('Test is running for more than 1 minute.');
|
|
||||||
this.logger.warning('Consider using --timeout option to set a timeout for test files.');
|
|
||||||
this.logger.warning('Example: tstest test --timeout=300 (for 5 minutes)');
|
|
||||||
}, 60000); // 1 minute
|
|
||||||
}
|
|
||||||
|
|
||||||
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()];
|
||||||
|
|
||||||
@ -92,15 +82,80 @@ export class TsTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the timeout warning timer if it was set
|
|
||||||
if (this.timeoutWarningTimer) {
|
|
||||||
clearTimeout(this.timeoutWarningTimer);
|
|
||||||
this.timeoutWarningTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
tapCombinator.evaluate();
|
tapCombinator.evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async runWatch(ignorePatterns: string[] = []) {
|
||||||
|
const smartchokInstance = new plugins.smartchok.Smartchok([this.testDir.cwd]);
|
||||||
|
|
||||||
|
console.clear();
|
||||||
|
this.logger.watchModeStart();
|
||||||
|
|
||||||
|
// Initial run
|
||||||
|
await this.run();
|
||||||
|
|
||||||
|
// Set up file watcher
|
||||||
|
const fileChanges = new Map<string, NodeJS.Timeout>();
|
||||||
|
const debounceTime = 300; // 300ms debounce
|
||||||
|
|
||||||
|
const runTestsAfterChange = async () => {
|
||||||
|
console.clear();
|
||||||
|
const changedFiles = Array.from(fileChanges.keys());
|
||||||
|
fileChanges.clear();
|
||||||
|
|
||||||
|
this.logger.watchModeRerun(changedFiles);
|
||||||
|
await this.run();
|
||||||
|
this.logger.watchModeWaiting();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start watching before subscribing to events
|
||||||
|
await smartchokInstance.start();
|
||||||
|
|
||||||
|
// Subscribe to file change events
|
||||||
|
const changeObservable = await smartchokInstance.getObservableFor('change');
|
||||||
|
const addObservable = await smartchokInstance.getObservableFor('add');
|
||||||
|
const unlinkObservable = await smartchokInstance.getObservableFor('unlink');
|
||||||
|
|
||||||
|
const handleFileChange = (changedPath: string) => {
|
||||||
|
// Skip if path matches ignore patterns
|
||||||
|
if (ignorePatterns.some(pattern => changedPath.includes(pattern))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing timeout for this file if any
|
||||||
|
if (fileChanges.has(changedPath)) {
|
||||||
|
clearTimeout(fileChanges.get(changedPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout for this file
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
fileChanges.delete(changedPath);
|
||||||
|
if (fileChanges.size === 0) {
|
||||||
|
runTestsAfterChange();
|
||||||
|
}
|
||||||
|
}, debounceTime);
|
||||||
|
|
||||||
|
fileChanges.set(changedPath, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to all relevant events
|
||||||
|
changeObservable.subscribe(([path]) => handleFileChange(path));
|
||||||
|
addObservable.subscribe(([path]) => handleFileChange(path));
|
||||||
|
unlinkObservable.subscribe(([path]) => handleFileChange(path));
|
||||||
|
|
||||||
|
this.logger.watchModeWaiting();
|
||||||
|
|
||||||
|
// Handle Ctrl+C to exit gracefully
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
this.logger.watchModeStop();
|
||||||
|
await smartchokInstance.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep the process running
|
||||||
|
await new Promise(() => {}); // This promise never resolves
|
||||||
|
}
|
||||||
|
|
||||||
private async runSingleTestOrSkip(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
private async runSingleTestOrSkip(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
||||||
// Check if this file should be skipped based on range
|
// Check if this file should be skipped based on range
|
||||||
if (this.startFromFile !== null && fileIndex < this.startFromFile) {
|
if (this.startFromFile !== null && fileIndex < this.startFromFile) {
|
||||||
@ -201,6 +256,19 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
execResultStreaming.childProcess.on('error', cleanup);
|
execResultStreaming.childProcess.on('error', cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start warning timer if no timeout was specified
|
||||||
|
let warningTimer: NodeJS.Timeout | null = null;
|
||||||
|
if (this.timeoutSeconds === null) {
|
||||||
|
warningTimer = setTimeout(() => {
|
||||||
|
console.error('');
|
||||||
|
console.error(cs('⚠️ WARNING: Test file is running for more than 1 minute', 'orange'));
|
||||||
|
console.error(cs(` File: ${fileNameArg}`, 'orange'));
|
||||||
|
console.error(cs(' Consider using --timeout option to set a timeout for test files.', 'orange'));
|
||||||
|
console.error(cs(' Example: tstest test --timeout=300 (for 5 minutes)', 'orange'));
|
||||||
|
console.error('');
|
||||||
|
}, 60000); // 1 minute
|
||||||
|
}
|
||||||
|
|
||||||
// Handle timeout if specified
|
// Handle timeout if specified
|
||||||
if (this.timeoutSeconds !== null) {
|
if (this.timeoutSeconds !== null) {
|
||||||
const timeoutMs = this.timeoutSeconds * 1000;
|
const timeoutMs = this.timeoutSeconds * 1000;
|
||||||
@ -222,6 +290,10 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
// Clear timeout if test completed successfully
|
// Clear timeout if test completed successfully
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Clear warning timer if it was set
|
||||||
|
if (warningTimer) {
|
||||||
|
clearTimeout(warningTimer);
|
||||||
|
}
|
||||||
// Handle timeout error
|
// Handle timeout error
|
||||||
tapParser.handleTimeout(this.timeoutSeconds);
|
tapParser.handleTimeout(this.timeoutSeconds);
|
||||||
// Ensure entire process tree is killed if still running
|
// Ensure entire process tree is killed if still running
|
||||||
@ -236,6 +308,11 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear warning timer if it was set
|
||||||
|
if (warningTimer) {
|
||||||
|
clearTimeout(warningTimer);
|
||||||
|
}
|
||||||
|
|
||||||
return tapParser;
|
return tapParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,6 +431,19 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start warning timer if no timeout was specified
|
||||||
|
let warningTimer: NodeJS.Timeout | null = null;
|
||||||
|
if (this.timeoutSeconds === null) {
|
||||||
|
warningTimer = setTimeout(() => {
|
||||||
|
console.error('');
|
||||||
|
console.error(cs('⚠️ WARNING: Test file is running for more than 1 minute', 'orange'));
|
||||||
|
console.error(cs(` File: ${fileNameArg}`, 'orange'));
|
||||||
|
console.error(cs(' Consider using --timeout option to set a timeout for test files.', 'orange'));
|
||||||
|
console.error(cs(' Example: tstest test --timeout=300 (for 5 minutes)', 'orange'));
|
||||||
|
console.error('');
|
||||||
|
}, 60000); // 1 minute
|
||||||
|
}
|
||||||
|
|
||||||
// Handle timeout if specified
|
// Handle timeout if specified
|
||||||
if (this.timeoutSeconds !== null) {
|
if (this.timeoutSeconds !== null) {
|
||||||
const timeoutMs = this.timeoutSeconds * 1000;
|
const timeoutMs = this.timeoutSeconds * 1000;
|
||||||
@ -373,6 +463,10 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
// Clear timeout if test completed successfully
|
// Clear timeout if test completed successfully
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Clear warning timer if it was set
|
||||||
|
if (warningTimer) {
|
||||||
|
clearTimeout(warningTimer);
|
||||||
|
}
|
||||||
// Handle timeout error
|
// Handle timeout error
|
||||||
tapParser.handleTimeout(this.timeoutSeconds);
|
tapParser.handleTimeout(this.timeoutSeconds);
|
||||||
}
|
}
|
||||||
@ -380,6 +474,11 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
await evaluatePromise;
|
await evaluatePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear warning timer if it was set
|
||||||
|
if (warningTimer) {
|
||||||
|
clearTimeout(warningTimer);
|
||||||
|
}
|
||||||
|
|
||||||
// Always clean up resources, even on timeout
|
// Always clean up resources, even on timeout
|
||||||
try {
|
try {
|
||||||
await this.smartbrowserInstance.stop();
|
await this.smartbrowserInstance.stop();
|
||||||
@ -417,10 +516,10 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete 00err and 00diff directories if they exist
|
// Delete 00err and 00diff directories if they exist
|
||||||
if (await plugins.smartfile.fs.isDirectory(errDir)) {
|
if (plugins.smartfile.fs.isDirectorySync(errDir)) {
|
||||||
plugins.smartfile.fs.removeSync(errDir);
|
plugins.smartfile.fs.removeSync(errDir);
|
||||||
}
|
}
|
||||||
if (await plugins.smartfile.fs.isDirectory(diffDir)) {
|
if (plugins.smartfile.fs.isDirectorySync(diffDir)) {
|
||||||
plugins.smartfile.fs.removeSync(diffDir);
|
plugins.smartfile.fs.removeSync(diffDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,9 +242,11 @@ export class TsTestLogger {
|
|||||||
|
|
||||||
if (!this.options.quiet) {
|
if (!this.options.quiet) {
|
||||||
const total = passed + failed;
|
const total = passed + failed;
|
||||||
const status = failed === 0 ? 'PASSED' : 'FAILED';
|
if (failed === 0) {
|
||||||
const color = failed === 0 ? 'green' : 'red';
|
this.log(this.format(` Summary: ${passed}/${total} PASSED`, 'green'));
|
||||||
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
} else {
|
||||||
|
this.log(this.format(` Summary: ${passed} passed, ${failed} failed of ${total} tests`, 'red'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If using --logfile, handle error copy and diff detection
|
// If using --logfile, handle error copy and diff detection
|
||||||
@ -390,7 +392,11 @@ export class TsTestLogger {
|
|||||||
|
|
||||||
if (this.options.quiet) {
|
if (this.options.quiet) {
|
||||||
const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
|
const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
|
||||||
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
|
if (summary.totalFailed === 0) {
|
||||||
|
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
|
||||||
|
} else {
|
||||||
|
this.log(`\nSummary: ${summary.totalPassed} passed, ${summary.totalFailed} failed of ${summary.totalTests} tests | ${totalDuration}ms | ${status}`);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,15 +431,23 @@ export class TsTestLogger {
|
|||||||
|
|
||||||
// Performance metrics
|
// Performance metrics
|
||||||
if (this.options.verbose) {
|
if (this.options.verbose) {
|
||||||
const avgDuration = Math.round(totalDuration / summary.totalTests);
|
// Calculate metrics based on actual test durations
|
||||||
const slowestTest = this.fileResults
|
const allTests = this.fileResults.flatMap(r => r.tests);
|
||||||
.flatMap(r => r.tests)
|
const testDurations = allTests.map(t => t.duration);
|
||||||
.sort((a, b) => b.duration - a.duration)[0];
|
const sumOfTestDurations = testDurations.reduce((sum, d) => sum + d, 0);
|
||||||
|
const avgTestDuration = allTests.length > 0 ? Math.round(sumOfTestDurations / allTests.length) : 0;
|
||||||
|
|
||||||
|
// Find slowest test (exclude 0ms durations unless all are 0)
|
||||||
|
const nonZeroDurations = allTests.filter(t => t.duration > 0);
|
||||||
|
const testsToSort = nonZeroDurations.length > 0 ? nonZeroDurations : allTests;
|
||||||
|
const slowestTest = testsToSort.sort((a, b) => b.duration - a.duration)[0];
|
||||||
|
|
||||||
this.log(this.format('\n⏱️ Performance Metrics:', 'cyan'));
|
this.log(this.format('\n⏱️ Performance Metrics:', 'cyan'));
|
||||||
this.log(this.format(` Average per test: ${avgDuration}ms`, 'white'));
|
this.log(this.format(` Average per test: ${avgTestDuration}ms`, 'white'));
|
||||||
if (slowestTest) {
|
if (slowestTest && slowestTest.duration > 0) {
|
||||||
this.log(this.format(` Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'yellow'));
|
this.log(this.format(` Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'orange'));
|
||||||
|
} else if (allTests.length > 0) {
|
||||||
|
this.log(this.format(` All tests completed in <1ms`, 'dim'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,4 +534,47 @@ export class TsTestLogger {
|
|||||||
|
|
||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch mode methods
|
||||||
|
watchModeStart() {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'watchModeStart' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(this.format('\n👀 Watch Mode', 'cyan'));
|
||||||
|
this.log(this.format(' Running tests in watch mode...', 'dim'));
|
||||||
|
this.log(this.format(' Press Ctrl+C to exit\n', 'dim'));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchModeWaiting() {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'watchModeWaiting' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(this.format('\n Waiting for file changes...', 'dim'));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchModeRerun(changedFiles: string[]) {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'watchModeRerun', changedFiles });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(this.format('\n🔄 File changes detected:', 'cyan'));
|
||||||
|
changedFiles.forEach(file => {
|
||||||
|
this.log(this.format(` • ${file}`, 'yellow'));
|
||||||
|
});
|
||||||
|
this.log(this.format('\n Re-running tests...\n', 'dim'));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchModeStop() {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'watchModeStop' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(this.format('\n\n👋 Stopping watch mode...', 'cyan'));
|
||||||
|
}
|
||||||
}
|
}
|
@ -13,6 +13,7 @@ export {
|
|||||||
// @push.rocks scope
|
// @push.rocks scope
|
||||||
import * as consolecolor from '@push.rocks/consolecolor';
|
import * as consolecolor from '@push.rocks/consolecolor';
|
||||||
import * as smartbrowser from '@push.rocks/smartbrowser';
|
import * as smartbrowser from '@push.rocks/smartbrowser';
|
||||||
|
import * as smartchok from '@push.rocks/smartchok';
|
||||||
import * as smartdelay from '@push.rocks/smartdelay';
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
@ -23,6 +24,7 @@ import * as tapbundle from '../dist_ts_tapbundle/index.js';
|
|||||||
export {
|
export {
|
||||||
consolecolor,
|
consolecolor,
|
||||||
smartbrowser,
|
smartbrowser,
|
||||||
|
smartchok,
|
||||||
smartdelay,
|
smartdelay,
|
||||||
smartfile,
|
smartfile,
|
||||||
smartlog,
|
smartlog,
|
||||||
@ -31,7 +33,7 @@ export {
|
|||||||
tapbundle,
|
tapbundle,
|
||||||
};
|
};
|
||||||
|
|
||||||
// @gitzone scope
|
// @git.zone scope
|
||||||
import * as tsbundle from '@git.zone/tsbundle';
|
import * as tsbundle from '@git.zone/tsbundle';
|
||||||
|
|
||||||
export { tsbundle };
|
export { tsbundle };
|
||||||
|
@ -269,8 +269,8 @@ export class ProtocolParser {
|
|||||||
|
|
||||||
// Extract simple key:value pairs
|
// Extract simple key:value pairs
|
||||||
const simpleMatch = line.match(new RegExp(`${this.escapeRegex(PROTOCOL_MARKERS.START)}([^${this.escapeRegex(PROTOCOL_MARKERS.END)}]+)${this.escapeRegex(PROTOCOL_MARKERS.END)}`));
|
const simpleMatch = line.match(new RegExp(`${this.escapeRegex(PROTOCOL_MARKERS.START)}([^${this.escapeRegex(PROTOCOL_MARKERS.END)}]+)${this.escapeRegex(PROTOCOL_MARKERS.END)}`));
|
||||||
if (simpleMatch && !simpleMatch[1].includes(':')) {
|
if (simpleMatch && simpleMatch[1].includes(':') && !simpleMatch[1].includes('META:') && !simpleMatch[1].includes('SKIP:') && !simpleMatch[1].includes('TODO:') && !simpleMatch[1].includes('EVENT:')) {
|
||||||
// Not a prefixed format, might be key:value pairs
|
// This is a simple key:value format (not a prefixed format)
|
||||||
const pairs = simpleMatch[1].split(',');
|
const pairs = simpleMatch[1].split(',');
|
||||||
for (const pair of pairs) {
|
for (const pair of pairs) {
|
||||||
const [key, value] = pair.split(':');
|
const [key, value] = pair.split(':');
|
||||||
|
Reference in New Issue
Block a user