Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b4ec78328 | |||
| f23c902658 |
11
changelog.md
11
changelog.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-03 - 3.2.0 - feat(tapbundle_serverside)
|
||||||
|
add network port discovery utilities and migrate file I/O to smartfs; refactor runtimes to use Node fs and SmartFs, update server APIs and bump dependencies
|
||||||
|
|
||||||
|
- Add tapNodeTools.findFreePort, findFreePorts and findFreePortRange to provide network port discovery and ranges for tests
|
||||||
|
- Integrate @push.rocks/smartfs (smartfsInstance) and replace many @push.rocks/smartfile.fs usages with smartfsInstance file/directory APIs
|
||||||
|
- Switch several internals to Node's fs (exported via plugins) and introduce SmartFileFactory.nodeFs() for file handling
|
||||||
|
- Replace smartchok with smartwatch for file watching and update watch/start/stop flows
|
||||||
|
- Update server instantiation to TypedServer and change addRoute usage to the new handler signature; serve bundled test directory
|
||||||
|
- Add tests for network tools and update migration/test code to use smartfsInstance
|
||||||
|
- Bump multiple dependencies (e.g. @api.global/typedserver, @push.rocks/smartfile/smartfs/smartnetwork/smarts3/smartwatch) and @git.zone/tsbuild
|
||||||
|
|
||||||
## 2026-01-25 - 3.1.8 - fix(tapbundle)
|
## 2026-01-25 - 3.1.8 - fix(tapbundle)
|
||||||
treat tests that call tools.allowFailure() as passing and update tests to use tools parameter
|
treat tests that call tools.allowFailure() as passing and update tests to use tools parameter
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
"target": "ES2022"
|
"target": "ES2022"
|
||||||
},
|
},
|
||||||
"nodeModulesDir": true,
|
"nodeModulesDir": true,
|
||||||
"version": "3.1.8"
|
"version": "3.2.0"
|
||||||
}
|
}
|
||||||
|
|||||||
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "3.1.8",
|
"version": "3.2.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": {
|
||||||
@@ -25,35 +25,36 @@
|
|||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^3.1.0",
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
"@types/node": "^22.15.21"
|
"@types/node": "^22.15.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedserver": "^3.0.80",
|
"@api.global/typedserver": "^8.4.0",
|
||||||
"@git.zone/tsbundle": "^2.5.2",
|
"@git.zone/tsbundle": "^2.9.0",
|
||||||
"@git.zone/tsrun": "^2.0.0",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@push.rocks/consolecolor": "^2.0.3",
|
"@push.rocks/consolecolor": "^2.0.3",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartbrowser": "^2.0.8",
|
"@push.rocks/smartbrowser": "^2.0.8",
|
||||||
"@push.rocks/smartchok": "^1.1.1",
|
|
||||||
"@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": "^6.0.0",
|
"@push.rocks/smartenv": "^6.0.0",
|
||||||
"@push.rocks/smartexpect": "^2.5.0",
|
"@push.rocks/smartexpect": "^2.5.0",
|
||||||
"@push.rocks/smartfile": "^11.2.7",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartjson": "^5.2.0",
|
"@push.rocks/smartfs": "^1.3.1",
|
||||||
"@push.rocks/smartlog": "^3.1.10",
|
"@push.rocks/smartjson": "^6.0.0",
|
||||||
"@push.rocks/smartmongo": "^2.0.14",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
|
"@push.rocks/smartmongo": "^5.1.0",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smarts3": "^3.0.0",
|
"@push.rocks/smarts3": "^5.3.0",
|
||||||
"@push.rocks/smartshell": "^3.3.0",
|
"@push.rocks/smartshell": "^3.3.0",
|
||||||
"@push.rocks/smarttime": "^4.1.1",
|
"@push.rocks/smartwatch": "^6.3.0",
|
||||||
|
"@push.rocks/smarttime": "^4.2.3",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"figures": "^6.1.0",
|
"figures": "^6.1.0",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
|||||||
2519
pnpm-lock.yaml
generated
2519
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ This project integrates tstest with tapbundle through a modular architecture:
|
|||||||
|
|
||||||
1. **tstest** (`/ts/`) - The test runner that discovers and executes test files
|
1. **tstest** (`/ts/`) - The test runner that discovers and executes test files
|
||||||
2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests
|
2. **tapbundle** (`/ts_tapbundle/`) - The TAP testing framework for writing tests
|
||||||
3. **tapbundle_serverside** (`/ts_tapbundle_serverside/`) - Server-side testing utilities (runCommand, env vars, HTTPS certs, MongoDB, S3, test assets)
|
3. **tapbundle_serverside** (`/ts_tapbundle_serverside/`) - Server-side testing utilities (network port finding, runCommand, env vars, HTTPS certs, MongoDB, S3, test assets)
|
||||||
|
|
||||||
## How Components Work Together
|
## How Components Work Together
|
||||||
|
|
||||||
|
|||||||
55
test/tapbundle/test.network-tools.node.ts
Normal file
55
test/tapbundle/test.network-tools.node.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { tap, expect } from '../../ts_tapbundle/index.js';
|
||||||
|
import { tapNodeTools } from '../../ts_tapbundle_serverside/index.js';
|
||||||
|
|
||||||
|
tap.test('should find a single free port', async () => {
|
||||||
|
const port = await tapNodeTools.findFreePort();
|
||||||
|
expect(port).toBeTypeOf('number');
|
||||||
|
expect(port).toBeGreaterThanOrEqual(3000);
|
||||||
|
expect(port).toBeLessThanOrEqual(60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should find a free port in a specific range', async () => {
|
||||||
|
const port = await tapNodeTools.findFreePort({
|
||||||
|
startPort: 8000,
|
||||||
|
endPort: 9000,
|
||||||
|
});
|
||||||
|
expect(port).toBeGreaterThanOrEqual(8000);
|
||||||
|
expect(port).toBeLessThanOrEqual(9000);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should find a free port with exclusions', async () => {
|
||||||
|
const port = await tapNodeTools.findFreePort({
|
||||||
|
startPort: 10000,
|
||||||
|
endPort: 10100,
|
||||||
|
exclude: [10000, 10001, 10002],
|
||||||
|
});
|
||||||
|
expect(port).toBeGreaterThanOrEqual(10000);
|
||||||
|
expect(port).toBeLessThanOrEqual(10100);
|
||||||
|
expect(port).not.toEqual(10000);
|
||||||
|
expect(port).not.toEqual(10001);
|
||||||
|
expect(port).not.toEqual(10002);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should find multiple free ports', async () => {
|
||||||
|
const ports = await tapNodeTools.findFreePorts(3);
|
||||||
|
expect(ports).toHaveLength(3);
|
||||||
|
// All ports should be distinct
|
||||||
|
const uniquePorts = new Set(ports);
|
||||||
|
expect(uniquePorts.size).toEqual(3);
|
||||||
|
for (const port of ports) {
|
||||||
|
expect(port).toBeGreaterThanOrEqual(3000);
|
||||||
|
expect(port).toBeLessThanOrEqual(60000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should find a consecutive port range', async () => {
|
||||||
|
const ports = await tapNodeTools.findFreePortRange(3, {
|
||||||
|
startPort: 20000,
|
||||||
|
endPort: 30000,
|
||||||
|
});
|
||||||
|
expect(ports).toHaveLength(3);
|
||||||
|
expect(ports[1]).toEqual(ports[0] + 1);
|
||||||
|
expect(ports[2]).toEqual(ports[0] + 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
||||||
@@ -37,10 +37,11 @@ tap.test('Migration - generateReport works', async () => {
|
|||||||
tap.test('Migration - detects legacy files when they exist', async () => {
|
tap.test('Migration - detects legacy files when they exist', async () => {
|
||||||
// Create a temporary legacy test file
|
// Create a temporary legacy test file
|
||||||
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration');
|
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration');
|
||||||
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
|
try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
|
||||||
|
await plugins.smartfsInstance.directory(tempDir).recursive().create();
|
||||||
|
|
||||||
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
|
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
|
||||||
await plugins.smartfile.memory.toFs('// Legacy test file\nexport default Promise.resolve();', legacyFile);
|
await plugins.smartfsInstance.file(legacyFile).write('// Legacy test file\nexport default Promise.resolve();');
|
||||||
|
|
||||||
const migration = new Migration({
|
const migration = new Migration({
|
||||||
baseDir: tempDir,
|
baseDir: tempDir,
|
||||||
@@ -53,18 +54,19 @@ tap.test('Migration - detects legacy files when they exist', async () => {
|
|||||||
expect(legacyFiles[0]).toContain('test.browser.ts');
|
expect(legacyFiles[0]).toContain('test.browser.ts');
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
await plugins.smartfile.fs.removeSync(tempDir);
|
plugins.fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Migration - detects both legacy pattern', async () => {
|
tap.test('Migration - detects both legacy pattern', async () => {
|
||||||
// Create temporary legacy files
|
// Create temporary legacy files
|
||||||
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_both');
|
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_both');
|
||||||
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
|
try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
|
||||||
|
await plugins.smartfsInstance.directory(tempDir).recursive().create();
|
||||||
|
|
||||||
const browserFile = plugins.path.join(tempDir, 'test.browser.ts');
|
const browserFile = plugins.path.join(tempDir, 'test.browser.ts');
|
||||||
const bothFile = plugins.path.join(tempDir, 'test.both.ts');
|
const bothFile = plugins.path.join(tempDir, 'test.both.ts');
|
||||||
await plugins.smartfile.memory.toFs('// Browser test\nexport default Promise.resolve();', browserFile);
|
await plugins.smartfsInstance.file(browserFile).write('// Browser test\nexport default Promise.resolve();');
|
||||||
await plugins.smartfile.memory.toFs('// Both test\nexport default Promise.resolve();', bothFile);
|
await plugins.smartfsInstance.file(bothFile).write('// Both test\nexport default Promise.resolve();');
|
||||||
|
|
||||||
const migration = new Migration({
|
const migration = new Migration({
|
||||||
baseDir: tempDir,
|
baseDir: tempDir,
|
||||||
@@ -76,16 +78,17 @@ tap.test('Migration - detects both legacy pattern', async () => {
|
|||||||
expect(legacyFiles.length).toEqual(2);
|
expect(legacyFiles.length).toEqual(2);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
await plugins.smartfile.fs.removeSync(tempDir);
|
plugins.fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Migration - dry run does not modify files', async () => {
|
tap.test('Migration - dry run does not modify files', async () => {
|
||||||
// Create a temporary legacy test file
|
// Create a temporary legacy test file
|
||||||
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_dryrun');
|
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_dryrun');
|
||||||
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
|
try { await plugins.smartfsInstance.directory(tempDir).recursive().delete(); } catch (e) { /* may not exist */ }
|
||||||
|
await plugins.smartfsInstance.directory(tempDir).recursive().create();
|
||||||
|
|
||||||
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
|
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
|
||||||
await plugins.smartfile.memory.toFs('// Legacy test file\nexport default Promise.resolve();', legacyFile);
|
await plugins.smartfsInstance.file(legacyFile).write('// Legacy test file\nexport default Promise.resolve();');
|
||||||
|
|
||||||
const migration = new Migration({
|
const migration = new Migration({
|
||||||
baseDir: tempDir,
|
baseDir: tempDir,
|
||||||
@@ -101,11 +104,11 @@ tap.test('Migration - dry run does not modify files', async () => {
|
|||||||
expect(summary.migratedCount).toEqual(1); // Dry run still counts as "would migrate"
|
expect(summary.migratedCount).toEqual(1); // Dry run still counts as "would migrate"
|
||||||
|
|
||||||
// Verify original file still exists
|
// Verify original file still exists
|
||||||
const fileExists = await plugins.smartfile.fs.fileExists(legacyFile);
|
const fileExists = await plugins.smartfsInstance.file(legacyFile).exists();
|
||||||
expect(fileExists).toEqual(true);
|
expect(fileExists).toEqual(true);
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
await plugins.smartfile.fs.removeSync(tempDir);
|
plugins.fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '3.1.8',
|
version: '3.2.0',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'a test utility to run tests that match test/**/*.ts'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,10 +111,7 @@ export class Migration {
|
|||||||
* Find all legacy test files in the base directory
|
* Find all legacy test files in the base directory
|
||||||
*/
|
*/
|
||||||
async findLegacyFiles(): Promise<string[]> {
|
async findLegacyFiles(): Promise<string[]> {
|
||||||
const files = await plugins.smartfile.fs.listFileTree(
|
const files = plugins.fs.globSync(this.options.pattern, { cwd: this.options.baseDir }) as string[];
|
||||||
this.options.baseDir,
|
|
||||||
this.options.pattern
|
|
||||||
);
|
|
||||||
|
|
||||||
const legacyFiles: string[] = [];
|
const legacyFiles: string[] = [];
|
||||||
|
|
||||||
@@ -154,7 +151,7 @@ export class Migration {
|
|||||||
const newPath = plugins.path.join(dirName, newFileName);
|
const newPath = plugins.path.join(dirName, newFileName);
|
||||||
|
|
||||||
// Check if target file already exists
|
// Check if target file already exists
|
||||||
if (await plugins.smartfile.fs.fileExists(newPath)) {
|
if (await plugins.smartfsInstance.file(newPath).exists()) {
|
||||||
return {
|
return {
|
||||||
oldPath: filePath,
|
oldPath: filePath,
|
||||||
newPath,
|
newPath,
|
||||||
@@ -206,7 +203,7 @@ export class Migration {
|
|||||||
private async isGitRepository(dir: string): Promise<boolean> {
|
private async isGitRepository(dir: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const gitDir = plugins.path.join(dir, '.git');
|
const gitDir = plugins.path.join(dir, '.git');
|
||||||
return await plugins.smartfile.fs.isDirectory(gitDir);
|
return await plugins.smartfsInstance.directory(gitDir).exists();
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
|
|||||||
// Check for 00init.ts file in test directory
|
// Check for 00init.ts file in test directory
|
||||||
const testDir = plugins.path.dirname(testFile);
|
const testDir = plugins.path.dirname(testFile);
|
||||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
||||||
|
|
||||||
let runCommand = fullCommand;
|
let runCommand = fullCommand;
|
||||||
let loaderPath: string | null = null;
|
let loaderPath: string | null = null;
|
||||||
@@ -135,7 +135,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|||||||
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||||
`;
|
`;
|
||||||
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
||||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
||||||
|
|
||||||
// Rebuild command with loader file
|
// Rebuild command with loader file
|
||||||
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
||||||
@@ -148,8 +148,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
if (loaderPath) {
|
if (loaderPath) {
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
try {
|
try {
|
||||||
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
if (plugins.fs.existsSync(loaderPath)) {
|
||||||
plugins.smartfile.fs.removeSync(loaderPath);
|
plugins.fs.rmSync(loaderPath, { force: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
|||||||
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
|
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
|
||||||
|
|
||||||
// lets bundle the test
|
// lets bundle the test
|
||||||
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath);
|
try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
|
||||||
|
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
|
||||||
await this.tsbundleInstance.build(process.cwd(), testFile, bundleFilePath, {
|
await this.tsbundleInstance.build(process.cwd(), testFile, bundleFilePath, {
|
||||||
bundler: 'esbuild',
|
bundler: 'esbuild',
|
||||||
});
|
});
|
||||||
@@ -116,15 +117,13 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
|||||||
const { httpPort, wsPort } = await this.findFreePorts();
|
const { httpPort, wsPort } = await this.findFreePorts();
|
||||||
|
|
||||||
// lets create a server
|
// lets create a server
|
||||||
const server = new plugins.typedserver.servertools.Server({
|
const server = new plugins.typedserver.TypedServer({
|
||||||
cors: true,
|
cors: true,
|
||||||
port: httpPort,
|
port: httpPort,
|
||||||
|
serveDir: tsbundleCacheDirPath,
|
||||||
});
|
});
|
||||||
server.addRoute(
|
server.addRoute('/test', 'GET', async () => {
|
||||||
'/test',
|
return new Response(`
|
||||||
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
|
|
||||||
res.type('.html');
|
|
||||||
res.write(`
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script>
|
<script>
|
||||||
@@ -134,11 +133,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
|||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`, { headers: { 'Content-Type': 'text/html' } });
|
||||||
res.end();
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
|
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
// lets handle realtime comms
|
// lets handle realtime comms
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|||||||
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
|
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
|
||||||
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
|
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
|
||||||
|
|
||||||
if (plugins.smartfile.fs.fileExistsSync(denoJsonPath)) {
|
if (plugins.fs.existsSync(denoJsonPath)) {
|
||||||
configPath = denoJsonPath;
|
configPath = denoJsonPath;
|
||||||
} else if (plugins.smartfile.fs.fileExistsSync(denoJsoncPath)) {
|
} else if (plugins.fs.existsSync(denoJsoncPath)) {
|
||||||
configPath = denoJsoncPath;
|
configPath = denoJsoncPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|||||||
// Check for 00init.ts file in test directory
|
// Check for 00init.ts file in test directory
|
||||||
const testDir = plugins.path.dirname(testFile);
|
const testDir = plugins.path.dirname(testFile);
|
||||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
||||||
|
|
||||||
let runCommand = fullCommand;
|
let runCommand = fullCommand;
|
||||||
let loaderPath: string | null = null;
|
let loaderPath: string | null = null;
|
||||||
@@ -187,7 +187,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|||||||
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||||
`;
|
`;
|
||||||
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
||||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
||||||
|
|
||||||
// Rebuild command with loader file
|
// Rebuild command with loader file
|
||||||
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
||||||
@@ -200,8 +200,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
if (loaderPath) {
|
if (loaderPath) {
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
try {
|
try {
|
||||||
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
if (plugins.fs.existsSync(loaderPath)) {
|
||||||
plugins.smartfile.fs.removeSync(loaderPath);
|
plugins.fs.rmSync(loaderPath, { force: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if Dockerfile exists
|
// Check if Dockerfile exists
|
||||||
if (!await plugins.smartfile.fs.fileExists(dockerfilePath)) {
|
if (!await plugins.smartfsInstance.file(dockerfilePath).exists()) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Dockerfile not found: ${dockerfilePath}\n` +
|
`Dockerfile not found: ${dockerfilePath}\n` +
|
||||||
`Expected Dockerfile for Docker test variant.`
|
`Expected Dockerfile for Docker test variant.`
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
|||||||
// Check for 00init.ts file in test directory
|
// Check for 00init.ts file in test directory
|
||||||
const testDir = plugins.path.dirname(testFile);
|
const testDir = plugins.path.dirname(testFile);
|
||||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
||||||
|
|
||||||
// Determine which file to run
|
// Determine which file to run
|
||||||
let fileToRun = testFile;
|
let fileToRun = testFile;
|
||||||
@@ -138,7 +138,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|||||||
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||||
`;
|
`;
|
||||||
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
||||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
||||||
fileToRun = loaderPath;
|
fileToRun = loaderPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,8 +150,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
if (loaderPath) {
|
if (loaderPath) {
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
try {
|
try {
|
||||||
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
if (plugins.fs.existsSync(loaderPath)) {
|
||||||
plugins.smartfile.fs.removeSync(loaderPath);
|
plugins.fs.rmSync(loaderPath, { force: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
|
|||||||
@@ -497,12 +497,10 @@ export class TapParser {
|
|||||||
*/
|
*/
|
||||||
private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
|
private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
|
||||||
try {
|
try {
|
||||||
const smartfile = await import('@push.rocks/smartfile');
|
|
||||||
|
|
||||||
if (snapshotData.action === 'compare') {
|
if (snapshotData.action === 'compare') {
|
||||||
// Try to read existing snapshot
|
// Try to read existing snapshot
|
||||||
try {
|
try {
|
||||||
const existingSnapshot = await smartfile.fs.toStringSync(snapshotData.path);
|
const existingSnapshot = await plugins.smartfsInstance.file(snapshotData.path).encoding('utf8').read() as string;
|
||||||
if (existingSnapshot !== snapshotData.content) {
|
if (existingSnapshot !== snapshotData.content) {
|
||||||
// Snapshot mismatch
|
// Snapshot mismatch
|
||||||
if (this.logger) {
|
if (this.logger) {
|
||||||
@@ -520,8 +518,8 @@ export class TapParser {
|
|||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
// Snapshot doesn't exist, create it
|
// Snapshot doesn't exist, create it
|
||||||
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
||||||
await smartfile.fs.ensureDir(dirPath);
|
await plugins.smartfsInstance.directory(dirPath).recursive().create();
|
||||||
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
|
await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
|
||||||
if (this.logger) {
|
if (this.logger) {
|
||||||
this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
|
this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
|
||||||
}
|
}
|
||||||
@@ -532,8 +530,8 @@ export class TapParser {
|
|||||||
} else if (snapshotData.action === 'update') {
|
} else if (snapshotData.action === 'update') {
|
||||||
// Update snapshot
|
// Update snapshot
|
||||||
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
||||||
await smartfile.fs.ensureDir(dirPath);
|
await plugins.smartfsInstance.directory(dirPath).recursive().create();
|
||||||
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
|
await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
|
||||||
if (this.logger) {
|
if (this.logger) {
|
||||||
this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
|
this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import * as plugins from './tstest.plugins.js';
|
import * as plugins from './tstest.plugins.js';
|
||||||
import * as paths from './tstest.paths.js';
|
import * as paths from './tstest.paths.js';
|
||||||
import { SmartFile } from '@push.rocks/smartfile';
|
import { type SmartFile, SmartFileFactory } from '@push.rocks/smartfile';
|
||||||
import { TestExecutionMode } from './index.js';
|
import { TestExecutionMode } from './index.js';
|
||||||
|
|
||||||
|
const smartFileFactory = SmartFileFactory.nodeFs();
|
||||||
|
|
||||||
// tap related stuff
|
// tap related stuff
|
||||||
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
||||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||||
@@ -45,28 +47,28 @@ export class TestDirectory {
|
|||||||
switch (this.executionMode) {
|
switch (this.executionMode) {
|
||||||
case TestExecutionMode.FILE:
|
case TestExecutionMode.FILE:
|
||||||
// Single file mode
|
// Single file mode
|
||||||
const filePath = plugins.path.isAbsolute(this.testPath)
|
const filePath = plugins.path.isAbsolute(this.testPath)
|
||||||
? this.testPath
|
? this.testPath
|
||||||
: plugins.path.join(this.cwd, this.testPath);
|
: plugins.path.join(this.cwd, this.testPath);
|
||||||
|
|
||||||
if (await plugins.smartfile.fs.fileExists(filePath)) {
|
if (await plugins.smartfsInstance.file(filePath).exists()) {
|
||||||
this.testfileArray = [await plugins.smartfile.SmartFile.fromFilePath(filePath)];
|
this.testfileArray = [await smartFileFactory.fromFilePath(filePath)];
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Test file not found: ${filePath}`);
|
throw new Error(`Test file not found: ${filePath}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TestExecutionMode.GLOB:
|
case TestExecutionMode.GLOB:
|
||||||
// Glob pattern mode - use listFileTree which supports glob patterns
|
// Glob pattern mode - use Node.js fs.globSync for full glob support
|
||||||
const globPattern = this.testPath;
|
const globPattern = this.testPath;
|
||||||
const matchedFiles = await plugins.smartfile.fs.listFileTree(this.cwd, globPattern);
|
const matchedFiles = plugins.fs.globSync(globPattern, { cwd: this.cwd });
|
||||||
|
|
||||||
this.testfileArray = await Promise.all(
|
this.testfileArray = await Promise.all(
|
||||||
matchedFiles.map(async (filePath) => {
|
matchedFiles.map(async (filePath: string) => {
|
||||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||||
? filePath
|
? filePath
|
||||||
: plugins.path.join(this.cwd, filePath);
|
: plugins.path.join(this.cwd, filePath);
|
||||||
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
|
return await smartFileFactory.fromFilePath(absolutePath);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -79,19 +81,16 @@ export class TestDirectory {
|
|||||||
const tsPattern = '**/test*.ts';
|
const tsPattern = '**/test*.ts';
|
||||||
const dockerPattern = '**/*.docker.sh';
|
const dockerPattern = '**/*.docker.sh';
|
||||||
|
|
||||||
const [tsFiles, dockerFiles] = await Promise.all([
|
const tsFiles = plugins.fs.globSync(tsPattern, { cwd: dirPath });
|
||||||
plugins.smartfile.fs.listFileTree(dirPath, tsPattern),
|
const dockerFiles = plugins.fs.globSync(dockerPattern, { cwd: dirPath });
|
||||||
plugins.smartfile.fs.listFileTree(dirPath, dockerPattern),
|
const allTestFiles = [...tsFiles, ...dockerFiles] as string[];
|
||||||
]);
|
|
||||||
|
|
||||||
const allTestFiles = [...tsFiles, ...dockerFiles];
|
|
||||||
|
|
||||||
this.testfileArray = await Promise.all(
|
this.testfileArray = await Promise.all(
|
||||||
allTestFiles.map(async (filePath) => {
|
allTestFiles.map(async (filePath) => {
|
||||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||||
? filePath
|
? filePath
|
||||||
: plugins.path.join(dirPath, filePath);
|
: plugins.path.join(dirPath, filePath);
|
||||||
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
|
return await smartFileFactory.fromFilePath(absolutePath);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export class TsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async runWatch(ignorePatterns: string[] = []) {
|
public async runWatch(ignorePatterns: string[] = []) {
|
||||||
const smartchokInstance = new plugins.smartchok.Smartchok([this.testDir.cwd]);
|
const smartwatchInstance = new plugins.smartwatch.Smartwatch([this.testDir.cwd]);
|
||||||
|
|
||||||
console.clear();
|
console.clear();
|
||||||
this.logger.watchModeStart();
|
this.logger.watchModeStart();
|
||||||
@@ -155,12 +155,12 @@ export class TsTest {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Start watching before subscribing to events
|
// Start watching before subscribing to events
|
||||||
await smartchokInstance.start();
|
await smartwatchInstance.start();
|
||||||
|
|
||||||
// Subscribe to file change events
|
// Subscribe to file change events
|
||||||
const changeObservable = await smartchokInstance.getObservableFor('change');
|
const changeObservable = await smartwatchInstance.getObservableFor('change');
|
||||||
const addObservable = await smartchokInstance.getObservableFor('add');
|
const addObservable = await smartwatchInstance.getObservableFor('add');
|
||||||
const unlinkObservable = await smartchokInstance.getObservableFor('unlink');
|
const unlinkObservable = await smartwatchInstance.getObservableFor('unlink');
|
||||||
|
|
||||||
const handleFileChange = (changedPath: string) => {
|
const handleFileChange = (changedPath: string) => {
|
||||||
// Skip if path matches ignore patterns
|
// Skip if path matches ignore patterns
|
||||||
@@ -194,7 +194,7 @@ export class TsTest {
|
|||||||
// Handle Ctrl+C to exit gracefully
|
// Handle Ctrl+C to exit gracefully
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
this.logger.watchModeStop();
|
this.logger.watchModeStop();
|
||||||
await smartchokInstance.stop();
|
await smartwatchInstance.stop();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,8 +316,8 @@ export class TsTest {
|
|||||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||||
let runCommand = `tsrun ${fileNameArg}${tsrunOptions}`;
|
let runCommand = `tsrun ${fileNameArg}${tsrunOptions}`;
|
||||||
|
|
||||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
||||||
|
|
||||||
// If 00init.ts exists, run it first
|
// If 00init.ts exists, run it first
|
||||||
if (initFileExists) {
|
if (initFileExists) {
|
||||||
// Create a temporary loader file that imports both 00init.ts and the test file
|
// Create a temporary loader file that imports both 00init.ts and the test file
|
||||||
@@ -328,7 +328,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|||||||
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||||
`;
|
`;
|
||||||
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
|
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
|
||||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
||||||
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
|
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,14 +339,14 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
|
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
try {
|
try {
|
||||||
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
if (plugins.fs.existsSync(loaderPath)) {
|
||||||
plugins.smartfile.fs.removeSync(loaderPath);
|
plugins.fs.rmSync(loaderPath, { force: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
execResultStreaming.childProcess.on('exit', cleanup);
|
execResultStreaming.childProcess.on('exit', cleanup);
|
||||||
execResultStreaming.childProcess.on('error', cleanup);
|
execResultStreaming.childProcess.on('error', cleanup);
|
||||||
}
|
}
|
||||||
@@ -445,7 +445,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
|
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
|
||||||
|
|
||||||
// lets bundle the test
|
// lets bundle the test
|
||||||
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath);
|
try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
|
||||||
|
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
|
||||||
await this.tsbundleInstance.build(process.cwd(), fileNameArg, bundleFilePath, {
|
await this.tsbundleInstance.build(process.cwd(), fileNameArg, bundleFilePath, {
|
||||||
bundler: 'esbuild',
|
bundler: 'esbuild',
|
||||||
});
|
});
|
||||||
@@ -454,15 +455,13 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
const { httpPort, wsPort } = await this.findFreePorts();
|
const { httpPort, wsPort } = await this.findFreePorts();
|
||||||
|
|
||||||
// lets create a server
|
// lets create a server
|
||||||
const server = new plugins.typedserver.servertools.Server({
|
const server = new plugins.typedserver.TypedServer({
|
||||||
cors: true,
|
cors: true,
|
||||||
port: httpPort,
|
port: httpPort,
|
||||||
|
serveDir: tsbundleCacheDirPath,
|
||||||
});
|
});
|
||||||
server.addRoute(
|
server.addRoute('/test', 'GET', async () => {
|
||||||
'/test',
|
return new Response(`
|
||||||
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
|
|
||||||
res.type('.html');
|
|
||||||
res.write(`
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script>
|
<script>
|
||||||
@@ -472,11 +471,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
</head>
|
</head>
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`, { headers: { 'Content-Type': 'text/html' } });
|
||||||
res.end();
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
|
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
// lets handle realtime comms
|
// lets handle realtime comms
|
||||||
@@ -640,34 +636,33 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete 00err and 00diff directories if they exist
|
// Delete 00err and 00diff directories if they exist
|
||||||
if (plugins.smartfile.fs.isDirectorySync(errDir)) {
|
if (plugins.fs.existsSync(errDir) && plugins.fs.statSync(errDir).isDirectory()) {
|
||||||
plugins.smartfile.fs.removeSync(errDir);
|
plugins.fs.rmSync(errDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
if (plugins.smartfile.fs.isDirectorySync(diffDir)) {
|
if (plugins.fs.existsSync(diffDir) && plugins.fs.statSync(diffDir).isDirectory()) {
|
||||||
plugins.smartfile.fs.removeSync(diffDir);
|
plugins.fs.rmSync(diffDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all .log files in log directory (not in subdirectories)
|
// Get all .log files in log directory (not in subdirectories)
|
||||||
const files = await plugins.smartfile.fs.listFileTree(logDir, '*.log');
|
const entries = await plugins.smartfsInstance.directory(logDir).filter('*.log').list();
|
||||||
const logFiles = files.filter((file: string) => !file.includes('/'));
|
const logFiles = entries.filter((entry) => entry.isFile).map((entry) => entry.name);
|
||||||
|
|
||||||
if (logFiles.length === 0) {
|
if (logFiles.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure previous directory exists
|
// Ensure previous directory exists
|
||||||
await plugins.smartfile.fs.ensureDir(previousDir);
|
await plugins.smartfsInstance.directory(previousDir).recursive().create();
|
||||||
|
|
||||||
// Move each log file to previous directory
|
// Move each log file to previous directory
|
||||||
for (const file of logFiles) {
|
for (const filename of logFiles) {
|
||||||
const filename = plugins.path.basename(file);
|
|
||||||
const sourcePath = plugins.path.join(logDir, filename);
|
const sourcePath = plugins.path.join(logDir, filename);
|
||||||
const destPath = plugins.path.join(previousDir, filename);
|
const destPath = plugins.path.join(previousDir, filename);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Copy file to new location and remove original
|
// Copy file to new location and remove original
|
||||||
await plugins.smartfile.fs.copy(sourcePath, destPath);
|
await plugins.smartfsInstance.file(sourcePath).copy(destPath);
|
||||||
await plugins.smartfile.fs.remove(sourcePath);
|
await plugins.smartfsInstance.file(sourcePath).delete();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Silently continue if a file can't be moved
|
// Silently continue if a file can't be moved
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// node native
|
// node native
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export { path };
|
export { fs, path };
|
||||||
|
|
||||||
// @apiglobal scope
|
// @apiglobal scope
|
||||||
import * as typedserver from '@api.global/typedserver';
|
import * as typedserver from '@api.global/typedserver';
|
||||||
@@ -13,25 +14,29 @@ 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 smartfs from '@push.rocks/smartfs';
|
||||||
|
const smartfsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
import * as smartshell from '@push.rocks/smartshell';
|
import * as smartshell from '@push.rocks/smartshell';
|
||||||
|
import * as smartwatch from '@push.rocks/smartwatch';
|
||||||
import * as tapbundle from '../dist_ts_tapbundle/index.js';
|
import * as tapbundle from '../dist_ts_tapbundle/index.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
consolecolor,
|
consolecolor,
|
||||||
smartbrowser,
|
smartbrowser,
|
||||||
smartchok,
|
|
||||||
smartdelay,
|
smartdelay,
|
||||||
smartfile,
|
smartfile,
|
||||||
|
smartfs,
|
||||||
|
smartfsInstance,
|
||||||
smartlog,
|
smartlog,
|
||||||
smartnetwork,
|
smartnetwork,
|
||||||
smartpromise,
|
smartpromise,
|
||||||
smartshell,
|
smartshell,
|
||||||
|
smartwatch,
|
||||||
tapbundle,
|
tapbundle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pnpm install --save-dev @git.zone/tstest
|
|||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -615,19 +615,21 @@ tap.test('should use context', async (tapTools) => {
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](../license) file within this repository.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District court Bremen HRB 35230 HB, Germany
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pnpm install --save-dev @git.zone/tstest
|
|||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -586,19 +586,21 @@ No runtime-specific APIs are used, making it truly portable.
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](../license) file within this repository.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District court Bremen HRB 35230 HB, Germany
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as plugins from './plugins.js';
|
|||||||
|
|
||||||
class TapNodeTools {
|
class TapNodeTools {
|
||||||
private smartshellInstance: plugins.smartshell.Smartshell;
|
private smartshellInstance: plugins.smartshell.Smartshell;
|
||||||
|
private smartnetworkInstance: plugins.smartnetwork.SmartNetwork;
|
||||||
public testFileProvider = new TestFileProvider();
|
public testFileProvider = new TestFileProvider();
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@@ -87,12 +88,136 @@ class TapNodeTools {
|
|||||||
public async createSmarts3() {
|
public async createSmarts3() {
|
||||||
const smarts3Mod = await import('@push.rocks/smarts3');
|
const smarts3Mod = await import('@push.rocks/smarts3');
|
||||||
const smarts3Instance = new smarts3Mod.Smarts3({
|
const smarts3Instance = new smarts3Mod.Smarts3({
|
||||||
port: 3003,
|
server: { port: 3003 },
|
||||||
cleanSlate: true,
|
storage: { cleanSlate: true },
|
||||||
});
|
});
|
||||||
await smarts3Instance.start();
|
await smarts3Instance.start();
|
||||||
return smarts3Instance;
|
return smarts3Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Network Tools
|
||||||
|
// ============
|
||||||
|
|
||||||
|
private getSmartNetwork(): plugins.smartnetwork.SmartNetwork {
|
||||||
|
if (!this.smartnetworkInstance) {
|
||||||
|
this.smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
||||||
|
}
|
||||||
|
return this.smartnetworkInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a single free port on the local machine.
|
||||||
|
*/
|
||||||
|
public async findFreePort(optionsArg?: {
|
||||||
|
startPort?: number;
|
||||||
|
endPort?: number;
|
||||||
|
randomize?: boolean;
|
||||||
|
exclude?: number[];
|
||||||
|
}): Promise<number> {
|
||||||
|
const options = {
|
||||||
|
startPort: 3000,
|
||||||
|
endPort: 60000,
|
||||||
|
randomize: true,
|
||||||
|
exclude: [] as number[],
|
||||||
|
...optionsArg,
|
||||||
|
};
|
||||||
|
const smartnetwork = this.getSmartNetwork();
|
||||||
|
const port = await smartnetwork.findFreePort(options.startPort, options.endPort, {
|
||||||
|
randomize: options.randomize,
|
||||||
|
exclude: options.exclude,
|
||||||
|
});
|
||||||
|
if (!port) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find a free port in range ${options.startPort}-${options.endPort}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find multiple distinct free ports on the local machine.
|
||||||
|
* Each found port is automatically excluded from subsequent searches.
|
||||||
|
*/
|
||||||
|
public async findFreePorts(countArg: number, optionsArg?: {
|
||||||
|
startPort?: number;
|
||||||
|
endPort?: number;
|
||||||
|
randomize?: boolean;
|
||||||
|
exclude?: number[];
|
||||||
|
}): Promise<number[]> {
|
||||||
|
const options = {
|
||||||
|
startPort: 3000,
|
||||||
|
endPort: 60000,
|
||||||
|
randomize: true,
|
||||||
|
exclude: [] as number[],
|
||||||
|
...optionsArg,
|
||||||
|
};
|
||||||
|
const smartnetwork = this.getSmartNetwork();
|
||||||
|
const ports: number[] = [];
|
||||||
|
const excluded = new Set(options.exclude);
|
||||||
|
|
||||||
|
for (let i = 0; i < countArg; i++) {
|
||||||
|
const port = await smartnetwork.findFreePort(options.startPort, options.endPort, {
|
||||||
|
randomize: options.randomize,
|
||||||
|
exclude: [...excluded],
|
||||||
|
});
|
||||||
|
if (!port) {
|
||||||
|
throw new Error(
|
||||||
|
`Could only find ${ports.length} of ${countArg} free ports in range ${options.startPort}-${options.endPort}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ports.push(port);
|
||||||
|
excluded.add(port);
|
||||||
|
}
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a range of consecutive free ports on the local machine.
|
||||||
|
* All returned ports are sequential (e.g., [4000, 4001, 4002]).
|
||||||
|
*/
|
||||||
|
public async findFreePortRange(countArg: number, optionsArg?: {
|
||||||
|
startPort?: number;
|
||||||
|
endPort?: number;
|
||||||
|
exclude?: number[];
|
||||||
|
}): Promise<number[]> {
|
||||||
|
const options = {
|
||||||
|
startPort: 3000,
|
||||||
|
endPort: 60000,
|
||||||
|
exclude: [] as number[],
|
||||||
|
...optionsArg,
|
||||||
|
};
|
||||||
|
const smartnetwork = this.getSmartNetwork();
|
||||||
|
const excludeSet = new Set(options.exclude);
|
||||||
|
|
||||||
|
for (let start = options.startPort; start <= options.endPort - countArg + 1; start++) {
|
||||||
|
let allFree = true;
|
||||||
|
for (let offset = 0; offset < countArg; offset++) {
|
||||||
|
const port = start + offset;
|
||||||
|
if (excludeSet.has(port)) {
|
||||||
|
allFree = false;
|
||||||
|
start = port; // skip ahead past excluded port
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const isFree = await smartnetwork.isLocalPortUnused(port);
|
||||||
|
if (!isFree) {
|
||||||
|
allFree = false;
|
||||||
|
start = port; // skip ahead past occupied port
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allFree) {
|
||||||
|
const ports: number[] = [];
|
||||||
|
for (let offset = 0; offset < countArg; offset++) {
|
||||||
|
ports.push(start + offset);
|
||||||
|
}
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Could not find ${countArg} consecutive free ports in range ${options.startPort}-${options.endPort}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tapNodeTools = new TapNodeTools();
|
export const tapNodeTools = new TapNodeTools();
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export class TestFileProvider {
|
|||||||
const response = await plugins.smartrequest.SmartRequest.create()
|
const response = await plugins.smartrequest.SmartRequest.create()
|
||||||
.url(fileUrls.dockerAlpineImage)
|
.url(fileUrls.dockerAlpineImage)
|
||||||
.get();
|
.get();
|
||||||
await plugins.smartfile.fs.ensureDir(paths.testFilesDir);
|
await plugins.smartfsInstance.directory(paths.testFilesDir).recursive().create();
|
||||||
const buffer = Buffer.from(await response.arrayBuffer());
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
await plugins.smartfile.memory.toFs(buffer, filePath);
|
await plugins.smartfsInstance.file(filePath).write(buffer);
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,11 @@ export { crypto,fs, path, };
|
|||||||
import * as qenv from '@push.rocks/qenv';
|
import * as qenv from '@push.rocks/qenv';
|
||||||
import * as smartcrypto from '@push.rocks/smartcrypto';
|
import * as smartcrypto from '@push.rocks/smartcrypto';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import * as smartfs from '@push.rocks/smartfs';
|
||||||
|
const smartfsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
|
||||||
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
import * as smartrequest from '@push.rocks/smartrequest';
|
import * as smartrequest from '@push.rocks/smartrequest';
|
||||||
import * as smartshell from '@push.rocks/smartshell';
|
import * as smartshell from '@push.rocks/smartshell';
|
||||||
|
|
||||||
export { qenv, smartcrypto, smartfile, smartpath, smartrequest, smartshell, };
|
export { qenv, smartcrypto, smartfile, smartfs, smartfsInstance, smartnetwork, smartpath, smartrequest, smartshell, };
|
||||||
|
|||||||
@@ -11,31 +11,31 @@ pnpm install --save-dev @git.zone/tstest
|
|||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For reporting bugs, issues, or security vulnerabilities, please visit https://community.foss.global/. This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a code.foss.global account to submit Pull Requests directly.
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`@git.zone/tstest/tapbundle_serverside` provides server-side testing utilities exclusively for Node.js runtime. These tools enable shell command execution, environment variable management, HTTPS certificate generation, database testing, object storage testing, and test asset management - all functionality that only makes sense on the server-side.
|
`@git.zone/tstest/tapbundle_serverside` provides server-side testing utilities exclusively for Node.js runtime. These tools make it trivial to spin up test infrastructure — free ports, HTTPS certificates, ephemeral databases, S3 storage, shell commands, and environment variable management.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- 🔐 **Environment Variables** - On-demand environment variable loading with qenv
|
- 🌐 **Network Utilities** — Find free ports and port ranges for test servers
|
||||||
- 💻 **Shell Commands** - Execute bash commands during tests
|
- 🔒 **HTTPS Certificates** — Generate self-signed certificates for testing
|
||||||
- 🔒 **HTTPS Certificates** - Generate self-signed certificates for testing
|
- 💻 **Shell Commands** — Execute bash commands during tests
|
||||||
- 🗄️ **MongoDB Testing** - Create ephemeral MongoDB instances
|
- 🔐 **Environment Variables** — On-demand environment variable loading with qenv
|
||||||
- 📦 **S3 Storage Testing** - Create local S3-compatible storage for tests
|
- 🗄️ **MongoDB Testing** — Create ephemeral MongoDB instances
|
||||||
- 📁 **Test File Management** - Download and manage test assets
|
- 📦 **S3 Storage Testing** — Create local S3-compatible storage
|
||||||
|
- 📁 **Test File Management** — Download and manage test assets
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
||||||
import { tap } from '@git.zone/tstest/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
tap.test('should use server-side tools', async () => {
|
tap.test('should start server on free port', async () => {
|
||||||
// Execute shell commands on the server-side
|
const port = await tapNodeTools.findFreePort();
|
||||||
const result = await tapNodeTools.runCommand('echo "hello"');
|
// start your server on `port`
|
||||||
console.log(result);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
@@ -47,340 +47,224 @@ export default tap.start();
|
|||||||
|
|
||||||
The main singleton instance providing all Node.js-specific utilities.
|
The main singleton instance providing all Node.js-specific utilities.
|
||||||
|
|
||||||
#### Environment Variables
|
---
|
||||||
|
|
||||||
##### `getQenv()`
|
### 🌐 Network Utilities
|
||||||
|
|
||||||
|
#### `findFreePort(options?)`
|
||||||
|
|
||||||
|
Find a single free port on the local machine.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Default: random free port in range 3000–60000
|
||||||
|
const port = await tapNodeTools.findFreePort();
|
||||||
|
|
||||||
|
// Custom range
|
||||||
|
const port = await tapNodeTools.findFreePort({
|
||||||
|
startPort: 8000,
|
||||||
|
endPort: 9000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// With exclusions and sequential scan
|
||||||
|
const port = await tapNodeTools.findFreePort({
|
||||||
|
startPort: 3000,
|
||||||
|
endPort: 60000,
|
||||||
|
randomize: false, // default: true
|
||||||
|
exclude: [8080, 8443],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `startPort` | `number` | `3000` | Start of port range (inclusive) |
|
||||||
|
| `endPort` | `number` | `60000` | End of port range (inclusive) |
|
||||||
|
| `randomize` | `boolean` | `true` | Pick a random port vs first available |
|
||||||
|
| `exclude` | `number[]` | `[]` | Ports to skip |
|
||||||
|
|
||||||
|
**Returns:** `Promise<number>` — Throws if no free port is found.
|
||||||
|
|
||||||
|
#### `findFreePorts(count, options?)`
|
||||||
|
|
||||||
|
Find multiple distinct free ports. Each found port is automatically excluded from subsequent searches, guaranteeing all returned ports are unique.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [httpPort, wsPort, adminPort] = await tapNodeTools.findFreePorts(3);
|
||||||
|
|
||||||
|
// With custom range
|
||||||
|
const ports = await tapNodeTools.findFreePorts(2, {
|
||||||
|
startPort: 10000,
|
||||||
|
endPort: 20000,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `count` — Number of ports to find
|
||||||
|
- `options` — Same as `findFreePort()`
|
||||||
|
|
||||||
|
**Returns:** `Promise<number[]>` — Array of distinct free ports.
|
||||||
|
|
||||||
|
#### `findFreePortRange(count, options?)`
|
||||||
|
|
||||||
|
Find consecutive free ports (e.g., `[4000, 4001, 4002]`). Useful when you need a contiguous block.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ports = await tapNodeTools.findFreePortRange(3, {
|
||||||
|
startPort: 20000,
|
||||||
|
endPort: 30000,
|
||||||
|
});
|
||||||
|
// ports = [N, N+1, N+2] where all three are free
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `startPort` | `number` | `3000` | Start of search range |
|
||||||
|
| `endPort` | `number` | `60000` | End of search range |
|
||||||
|
| `exclude` | `number[]` | `[]` | Ports to skip |
|
||||||
|
|
||||||
|
**Returns:** `Promise<number[]>` — Array of consecutive free ports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔒 HTTPS Certificates
|
||||||
|
|
||||||
|
#### `createHttpsCert(commonName?, allowSelfSigned?)`
|
||||||
|
|
||||||
|
Generate a self-signed HTTPS certificate for testing secure connections.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { key, cert } = await tapNodeTools.createHttpsCert('localhost', true);
|
||||||
|
|
||||||
|
// Use with Node.js https module
|
||||||
|
const server = https.createServer({ key, cert }, handler);
|
||||||
|
server.listen(port);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `commonName` (default: `'localhost'`) — Certificate common name
|
||||||
|
- `allowSelfSigned` (default: `true`) — Sets `NODE_TLS_REJECT_UNAUTHORIZED=0`
|
||||||
|
|
||||||
|
**Returns:** `Promise<{ key: string; cert: string }>` — PEM-encoded key and certificate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💻 Shell Commands
|
||||||
|
|
||||||
|
#### `runCommand(command)`
|
||||||
|
|
||||||
|
Execute a bash command and return the result.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await tapNodeTools.runCommand('ls -la');
|
||||||
|
console.log(result.exitCode);
|
||||||
|
console.log(result.stdout);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔐 Environment Variables
|
||||||
|
|
||||||
|
#### `getQenv()`
|
||||||
|
|
||||||
Get the qenv instance for managing environment variables from `.nogit/` directory.
|
Get the qenv instance for managing environment variables from `.nogit/` directory.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const qenv = await tapNodeTools.getQenv();
|
const qenv = await tapNodeTools.getQenv();
|
||||||
// qenv will load from .env files in .nogit/ directory
|
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `getEnvVarOnDemand(envVarName)`
|
#### `getEnvVarOnDemand(envVarName)`
|
||||||
|
|
||||||
Request an environment variable. If not available, qenv will prompt for it and store it securely.
|
Request an environment variable. If not available, qenv will prompt for it and store it securely.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
tap.test('should get API key', async () => {
|
const apiKey = await tapNodeTools.getEnvVarOnDemand('GITHUB_API_KEY');
|
||||||
const apiKey = await tapNodeTools.getEnvVarOnDemand('GITHUB_API_KEY');
|
// If not set, prompts interactively and stores in .nogit/.env
|
||||||
// If GITHUB_API_KEY is not set, qenv will prompt for it
|
|
||||||
// The value is stored in .nogit/.env for future use
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Use Cases:**
|
---
|
||||||
- API keys for integration tests
|
|
||||||
- Database credentials
|
|
||||||
- Service endpoints
|
|
||||||
- Any sensitive configuration needed for testing
|
|
||||||
|
|
||||||
#### Shell Commands
|
### 🗄️ Database Testing
|
||||||
|
|
||||||
##### `runCommand(command)`
|
#### `createSmartmongo()`
|
||||||
|
|
||||||
Execute a bash command and return the result.
|
Create an ephemeral MongoDB instance. Automatically started and ready to use.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
tap.test('should execute shell commands', async () => {
|
const mongo = await tapNodeTools.createSmartmongo();
|
||||||
const result = await tapNodeTools.runCommand('ls -la');
|
// ... run database tests ...
|
||||||
console.log(result.stdout);
|
await mongo.stop();
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Use Cases:**
|
Uses [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo).
|
||||||
- Setup test environment
|
|
||||||
- Execute CLI tools
|
|
||||||
- File system operations
|
|
||||||
- Process management
|
|
||||||
|
|
||||||
#### HTTPS Certificates
|
---
|
||||||
|
|
||||||
##### `createHttpsCert(commonName?, allowSelfSigned?)`
|
### 📦 Storage Testing
|
||||||
|
|
||||||
Generate a self-signed HTTPS certificate for testing secure connections.
|
#### `createSmarts3()`
|
||||||
|
|
||||||
|
Create a local S3-compatible storage instance for testing.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
tap.test('should create HTTPS server', async () => {
|
const s3 = await tapNodeTools.createSmarts3();
|
||||||
const { key, cert } = await tapNodeTools.createHttpsCert('localhost', true);
|
// ... run storage tests ...
|
||||||
|
await s3.stop();
|
||||||
// Use with Node.js https module
|
|
||||||
const server = https.createServer({ key, cert }, (req, res) => {
|
|
||||||
res.end('Hello Secure World');
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(3000);
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
Default config: port 3003, clean slate enabled. Uses [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3).
|
||||||
- `commonName` (optional): Certificate common name, default: 'localhost'
|
|
||||||
- `allowSelfSigned` (optional): Allow self-signed certificates by setting `NODE_TLS_REJECT_UNAUTHORIZED=0`, default: true
|
|
||||||
|
|
||||||
**Returns:**
|
---
|
||||||
- `key`: PEM-encoded private key
|
|
||||||
- `cert`: PEM-encoded certificate
|
|
||||||
|
|
||||||
**Use Cases:**
|
### 📁 Test File Provider
|
||||||
- Testing HTTPS servers
|
|
||||||
- Testing secure WebSocket connections
|
|
||||||
- Testing certificate validation logic
|
|
||||||
- Mocking secure external services
|
|
||||||
|
|
||||||
#### Database Testing
|
#### `testFileProvider.getDockerAlpineImageAsLocalTarball()`
|
||||||
|
|
||||||
##### `createSmartmongo()`
|
|
||||||
|
|
||||||
Create an ephemeral MongoDB instance for testing. Automatically started and ready to use.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
|
||||||
|
|
||||||
tap.test('should use MongoDB', async () => {
|
|
||||||
const mongoInstance = await tapNodeTools.createSmartmongo();
|
|
||||||
|
|
||||||
// Use the MongoDB instance
|
|
||||||
const db = await mongoInstance.getDatabase('testdb');
|
|
||||||
const collection = await db.getCollection('users');
|
|
||||||
|
|
||||||
await collection.insertOne({ name: 'Alice', age: 30 });
|
|
||||||
const user = await collection.findOne({ name: 'Alice' });
|
|
||||||
|
|
||||||
expect(user.age).toEqual(30);
|
|
||||||
|
|
||||||
// Cleanup (optional - instance will be cleaned up automatically)
|
|
||||||
await mongoInstance.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Ephemeral instance (starts fresh)
|
|
||||||
- Automatic cleanup
|
|
||||||
- Full MongoDB API via [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo)
|
|
||||||
|
|
||||||
**Use Cases:**
|
|
||||||
- Testing database operations
|
|
||||||
- Integration tests with MongoDB
|
|
||||||
- Testing data models
|
|
||||||
- Schema validation tests
|
|
||||||
|
|
||||||
#### Storage Testing
|
|
||||||
|
|
||||||
##### `createSmarts3()`
|
|
||||||
|
|
||||||
Create a local S3-compatible storage instance for testing object storage operations.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
|
||||||
|
|
||||||
tap.test('should use S3 storage', async () => {
|
|
||||||
const s3Instance = await tapNodeTools.createSmarts3();
|
|
||||||
|
|
||||||
// Use the S3 instance (MinIO-compatible API)
|
|
||||||
const bucket = await s3Instance.createBucket('test-bucket');
|
|
||||||
await bucket.putObject('file.txt', Buffer.from('Hello S3'));
|
|
||||||
const file = await bucket.getObject('file.txt');
|
|
||||||
|
|
||||||
expect(file.toString()).toEqual('Hello S3');
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
await s3Instance.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration:**
|
|
||||||
- Port: 3003 (default)
|
|
||||||
- Clean slate: true (starts fresh each time)
|
|
||||||
- Full S3-compatible API via [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3)
|
|
||||||
|
|
||||||
**Use Cases:**
|
|
||||||
- Testing file uploads/downloads
|
|
||||||
- Testing object storage operations
|
|
||||||
- Testing backup/restore logic
|
|
||||||
- Mocking cloud storage
|
|
||||||
|
|
||||||
### TestFileProvider
|
|
||||||
|
|
||||||
Utility for downloading and managing test assets.
|
|
||||||
|
|
||||||
#### `getDockerAlpineImageAsLocalTarball()`
|
|
||||||
|
|
||||||
Download the Alpine Linux Docker image as a tarball for testing.
|
Download the Alpine Linux Docker image as a tarball for testing.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
const tarballPath = await tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
|
||||||
|
// Path: ./.nogit/testfiles/alpine.tar
|
||||||
tap.test('should provide docker image', async () => {
|
|
||||||
const tarballPath = await tapNodeTools.testFileProvider.getDockerAlpineImageAsLocalTarball();
|
|
||||||
|
|
||||||
// Use the tarball path
|
|
||||||
// Path: ./.nogit/testfiles/alpine.tar
|
|
||||||
|
|
||||||
expect(tarballPath).toMatch(/alpine\.tar$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Downloads from https://code.foss.global/testassets/docker
|
|
||||||
- Caches in `.nogit/testfiles/` directory
|
|
||||||
- Returns local file path
|
|
||||||
|
|
||||||
**Use Cases:**
|
|
||||||
- Testing Docker operations
|
|
||||||
- Testing container deployment
|
|
||||||
- Testing image handling logic
|
|
||||||
|
|
||||||
### Path Utilities
|
|
||||||
|
|
||||||
The module exports useful path constants:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import * as paths from '@git.zone/tstest/tapbundle_serverside/paths';
|
|
||||||
|
|
||||||
console.log(paths.cwd); // Current working directory
|
|
||||||
console.log(paths.testFilesDir); // ./.nogit/testfiles/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Patterns and Best Practices
|
|
||||||
|
|
||||||
### Testing with External Services
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
|
||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
|
|
||||||
tap.describe('User Service Integration', () => {
|
|
||||||
let mongoInstance;
|
|
||||||
let db;
|
|
||||||
|
|
||||||
tap.beforeEach(async () => {
|
|
||||||
mongoInstance = await tapNodeTools.createSmartmongo();
|
|
||||||
db = await mongoInstance.getDatabase('testdb');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should create user', async () => {
|
|
||||||
const users = await db.getCollection('users');
|
|
||||||
await users.insertOne({ name: 'Bob', email: 'bob@example.com' });
|
|
||||||
|
|
||||||
const user = await users.findOne({ name: 'Bob' });
|
|
||||||
expect(user.email).toEqual('bob@example.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.afterEach(async () => {
|
|
||||||
await mongoInstance.stop();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing HTTPS Servers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
|
||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import * as https from 'https';
|
|
||||||
|
|
||||||
tap.test('should serve over HTTPS', async () => {
|
|
||||||
const { key, cert } = await tapNodeTools.createHttpsCert();
|
|
||||||
|
|
||||||
const server = https.createServer({ key, cert }, (req, res) => {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Secure Response');
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
server.listen(8443, () => resolve(undefined));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test the server
|
|
||||||
const response = await fetch('https://localhost:8443');
|
|
||||||
const text = await response.text();
|
|
||||||
expect(text).toEqual('Secure Response');
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment-Dependent Tests
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
|
|
||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
|
|
||||||
tap.test('should authenticate with GitHub', async () => {
|
|
||||||
const githubToken = await tapNodeTools.getEnvVarOnDemand('GITHUB_TOKEN');
|
|
||||||
|
|
||||||
// Use the token for API calls
|
|
||||||
const response = await fetch('https://api.github.com/user', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${githubToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.ok).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtime Requirements
|
## Runtime Requirements
|
||||||
|
|
||||||
⚠️ **Server-Side Only (Node.js)**: All utilities in this module are designed exclusively for server-side testing in Node.js runtime. They provide functionality like shell command execution, file system operations, and process management that only make sense on the server.
|
⚠️ **Node.js only.** All utilities in this module require Node.js. Import only in `.node.ts` test files.
|
||||||
|
|
||||||
**NOT available in:**
|
|
||||||
- Browser environments
|
|
||||||
- Deno runtime
|
|
||||||
- Bun runtime
|
|
||||||
|
|
||||||
**Important:** Import tapbundle_serverside only in tests that run exclusively on the server-side (`.node.ts` test files). For cross-runtime tests, these utilities will fail in non-Node environments.
|
|
||||||
|
|
||||||
## File Naming
|
|
||||||
|
|
||||||
Use Node.js-specific file naming when using these utilities:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
test/mytest.node.ts ✅ Node.js only
|
test/mytest.node.ts ✅ Correct — Node.js only
|
||||||
test/mytest.node+deno.ts ❌ Will fail in Deno
|
test/mytest.ts ✅ Correct — defaults to Node.js
|
||||||
test/mytest.browser+node.ts ⚠️ Browser won't have access to these tools
|
test/mytest.all.ts ❌ Will fail in Deno/Bun/Chromium
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
This module uses the following packages:
|
- [@push.rocks/smartnetwork](https://code.foss.global/push.rocks/smartnetwork) — Port discovery
|
||||||
- [@push.rocks/qenv](https://code.foss.global/push.rocks/qenv) - Environment variable management
|
- [@push.rocks/qenv](https://code.foss.global/push.rocks/qenv) — Environment variable management
|
||||||
- [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) - Shell command execution
|
- [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) — Shell command execution
|
||||||
- [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) - Certificate generation
|
- [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) — Certificate generation
|
||||||
- [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) - MongoDB testing
|
- [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) — MongoDB testing
|
||||||
- [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3) - S3 storage testing
|
- [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3) — S3 storage testing
|
||||||
- [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) - File operations
|
- [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) — File operations
|
||||||
- [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) - HTTP requests
|
- [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) — HTTP requests
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](../license) file within this repository.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District court Bremen HRB 35230 HB, Germany
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
|
|||||||
Reference in New Issue
Block a user