Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
ecf11efb4c | |||
1de674e91d | |||
9fa2c23ab2 | |||
36715c9139 | |||
ee0aca9ff7 | |||
aaebe75326 |
25
changelog.md
25
changelog.md
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-10-10 - 2.4.1 - fix(runtime/deno)
|
||||
Enable Deno runtime tests by adding required permissions and local settings
|
||||
|
||||
- ts/tstest.classes.runtime.deno.ts: expanded default Deno permissions to include --allow-net, --allow-write and --sloppy-imports to allow network access, file writes and permissive JS/TS imports
|
||||
- ts/tstest.classes.runtime.deno.ts: updated fallback permissions used when building the Deno command to match the new default set
|
||||
- Added .claude/settings.local.json with a set of allowed local commands/permissions used for local development/CI tooling
|
||||
|
||||
## 2025-10-10 - 2.4.0 - feat(runtime)
|
||||
Add runtime adapters, filename runtime parser and migration tool; integrate runtime selection into TsTest and add tests
|
||||
|
||||
- Introduce RuntimeAdapter abstraction and RuntimeAdapterRegistry to manage multiple runtimes
|
||||
- Add runtime adapters: NodeRuntimeAdapter, ChromiumRuntimeAdapter, DenoRuntimeAdapter and BunRuntimeAdapter
|
||||
- Add filename runtime parser utilities: parseTestFilename, isLegacyFilename and getLegacyMigrationTarget
|
||||
- Add Migration class to detect and (dry-run) migrate legacy test filenames to the new naming convention
|
||||
- Integrate runtime registry into TsTest and choose execution adapters based on parsed runtimes; show deprecation warnings for legacy naming
|
||||
- Add tests covering runtime parsing and migration: test/test.runtime.parser.node.ts and test/test.migration.node.ts
|
||||
|
||||
## 2025-09-12 - 2.3.8 - fix(tstest)
|
||||
Improve free port selection for Chrome runner and bump smartnetwork dependency
|
||||
|
||||
- Use randomized port selection when finding free HTTP and WebSocket ports to reduce collision probability in concurrent runs
|
||||
- Ensure WebSocket port search excludes the chosen HTTP port so the two ports will not conflict
|
||||
- Simplify failure handling: throw early if a free WebSocket port cannot be found instead of retrying with a less robust fallback
|
||||
- Bump @push.rocks/smartnetwork dependency from ^4.2.0 to ^4.4.0 to pick up new findFreePort options
|
||||
|
||||
## 2025-09-12 - 2.3.7 - fix(tests)
|
||||
Remove flaky dynamic-ports browser test and add local dev tool settings
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tstest",
|
||||
"version": "2.3.7",
|
||||
"version": "2.4.1",
|
||||
"private": false,
|
||||
"description": "a test utility to run tests that match test/**/*.ts",
|
||||
"exports": {
|
||||
@@ -43,7 +43,7 @@
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.1.9",
|
||||
"@push.rocks/smartmongo": "^2.0.12",
|
||||
"@push.rocks/smartnetwork": "^4.2.0",
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^4.3.1",
|
||||
|
302
pnpm-lock.yaml
generated
302
pnpm-lock.yaml
generated
@@ -54,8 +54,8 @@ importers:
|
||||
specifier: ^2.0.12
|
||||
version: 2.0.12(socks@2.8.7)
|
||||
'@push.rocks/smartnetwork':
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
'@push.rocks/smartpath':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
@@ -617,6 +617,9 @@ packages:
|
||||
resolution: {integrity: sha512-dOOXgzqaDoHu5qqMEPLKEgLz5CeIA7q8+1W62mCvFVCOqeC71UoTGJ4u1xUSOpIl2J1x2pqrNULkFteUeZW3/A==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
|
||||
'@leichtgewicht/ip-codec@2.0.5':
|
||||
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
||||
|
||||
'@lit-labs/ssr-dom-shim@1.4.0':
|
||||
resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==}
|
||||
|
||||
@@ -679,6 +682,39 @@ packages:
|
||||
'@pdf-lib/upng@1.0.1':
|
||||
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
|
||||
|
||||
'@peculiar/asn1-cms@2.5.0':
|
||||
resolution: {integrity: sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==}
|
||||
|
||||
'@peculiar/asn1-csr@2.5.0':
|
||||
resolution: {integrity: sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==}
|
||||
|
||||
'@peculiar/asn1-ecc@2.5.0':
|
||||
resolution: {integrity: sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==}
|
||||
|
||||
'@peculiar/asn1-pfx@2.5.0':
|
||||
resolution: {integrity: sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==}
|
||||
|
||||
'@peculiar/asn1-pkcs8@2.5.0':
|
||||
resolution: {integrity: sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==}
|
||||
|
||||
'@peculiar/asn1-pkcs9@2.5.0':
|
||||
resolution: {integrity: sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==}
|
||||
|
||||
'@peculiar/asn1-rsa@2.5.0':
|
||||
resolution: {integrity: sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==}
|
||||
|
||||
'@peculiar/asn1-schema@2.5.0':
|
||||
resolution: {integrity: sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==}
|
||||
|
||||
'@peculiar/asn1-x509-attr@2.5.0':
|
||||
resolution: {integrity: sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==}
|
||||
|
||||
'@peculiar/asn1-x509@2.5.0':
|
||||
resolution: {integrity: sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==}
|
||||
|
||||
'@peculiar/x509@1.14.0':
|
||||
resolution: {integrity: sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==}
|
||||
|
||||
'@pnpm/config.env-replace@1.1.0':
|
||||
resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
@@ -759,6 +795,9 @@ packages:
|
||||
'@push.rocks/smartdelay@3.0.5':
|
||||
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
||||
|
||||
'@push.rocks/smartdns@7.6.1':
|
||||
resolution: {integrity: sha512-nnP5+A2GOt0WsHrYhtKERmjdEHUchc+QbCCBEqlyeQTn+mNfx2WZvKVI1DFRJt8lamvzxP6Hr/BSe3WHdh4Snw==}
|
||||
|
||||
'@push.rocks/smartenv@5.0.13':
|
||||
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
|
||||
|
||||
@@ -816,8 +855,8 @@ packages:
|
||||
'@push.rocks/smartmongo@2.0.12':
|
||||
resolution: {integrity: sha512-NglYiO14BikxnlvW6JF18FtopBtaWQEGAtPxHmmSCbyhU8Mi0aEFO7VgCasE9Kguba/wcR597qhcDEdcpBg1eQ==}
|
||||
|
||||
'@push.rocks/smartnetwork@4.2.0':
|
||||
resolution: {integrity: sha512-XkRE2hQFCxUKzeD54MhbWlZAEVGyOhjU68A0zP9r3wsVjsVRqz38PwoM2GrhMrW4gtPa1j1t6cEUv1WNTwfo/Q==}
|
||||
'@push.rocks/smartnetwork@4.4.0':
|
||||
resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==}
|
||||
|
||||
'@push.rocks/smartnpm@2.0.6':
|
||||
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
||||
@@ -1390,6 +1429,9 @@ packages:
|
||||
'@tybys/wasm-util@0.10.0':
|
||||
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
|
||||
|
||||
'@types/bn.js@5.2.0':
|
||||
resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
@@ -1408,6 +1450,12 @@ packages:
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==}
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
resolution: {integrity: sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==}
|
||||
|
||||
'@types/express-serve-static-core@5.0.7':
|
||||
resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==}
|
||||
|
||||
@@ -1539,6 +1587,10 @@ packages:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
acme-client@5.4.0:
|
||||
resolution: {integrity: sha512-mORqg60S8iML6XSmVjqjGHJkINrCGLMj2QvDmFzI9vIlv1RGlyjmw3nrzaINJjkNsYXC41XhhD5pfy7CtuGcbA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -1581,6 +1633,10 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
asn1js@3.0.6:
|
||||
resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
ast-types@0.13.4:
|
||||
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1594,6 +1650,9 @@ packages:
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=}
|
||||
|
||||
axios@1.12.1:
|
||||
resolution: {integrity: sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==}
|
||||
|
||||
b4a@1.6.7:
|
||||
resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
|
||||
|
||||
@@ -1644,6 +1703,9 @@ packages:
|
||||
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
bn.js@4.12.2:
|
||||
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
|
||||
|
||||
body-parser@2.2.0:
|
||||
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1663,6 +1725,9 @@ packages:
|
||||
broadcast-channel@7.1.0:
|
||||
resolution: {integrity: sha512-InJljddsYWbEL8LBnopnCg+qMQp9KcowvYWOt4YWrjD5HmxzDYKdVbDS1w/ji5rFZdRD58V5UxJPtBdpEbEJYw==}
|
||||
|
||||
brorand@1.1.0:
|
||||
resolution: {integrity: sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=}
|
||||
|
||||
bson@6.10.4:
|
||||
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
|
||||
engines: {node: '>=16.20.1'}
|
||||
@@ -1953,6 +2018,10 @@ packages:
|
||||
resolution: {integrity: sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==}
|
||||
engines: {node: '>=4.5.0'}
|
||||
|
||||
dns-packet@5.6.1:
|
||||
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1963,6 +2032,9 @@ packages:
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
||||
|
||||
elliptic@6.6.1:
|
||||
resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -2293,6 +2365,9 @@ packages:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hash.js@1.1.7:
|
||||
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2310,6 +2385,9 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=}
|
||||
|
||||
html-minifier@4.0.0:
|
||||
resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2803,6 +2881,12 @@ packages:
|
||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
minimalistic-assert@1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
|
||||
minimalistic-crypto-utils@1.0.1:
|
||||
resolution: {integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=}
|
||||
|
||||
minimatch@10.0.3:
|
||||
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -3111,6 +3195,13 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
pvtsutils@1.3.6:
|
||||
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
|
||||
|
||||
pvutils@1.1.3:
|
||||
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
qs@6.14.0:
|
||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
||||
engines: {node: '>=0.6'}
|
||||
@@ -3143,6 +3234,9 @@ packages:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
|
||||
@@ -3484,6 +3578,10 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tsyringe@4.10.0:
|
||||
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
turndown-plugin-gfm@1.0.2:
|
||||
resolution: {integrity: sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==}
|
||||
|
||||
@@ -4918,6 +5016,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@leichtgewicht/ip-codec@2.0.5': {}
|
||||
|
||||
'@lit-labs/ssr-dom-shim@1.4.0': {}
|
||||
|
||||
'@lit/reactive-element@2.1.1':
|
||||
@@ -4991,6 +5091,96 @@ snapshots:
|
||||
dependencies:
|
||||
pako: 1.0.11
|
||||
|
||||
'@peculiar/asn1-cms@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
'@peculiar/asn1-x509': 2.5.0
|
||||
'@peculiar/asn1-x509-attr': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-csr@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
'@peculiar/asn1-x509': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-ecc@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
'@peculiar/asn1-x509': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pfx@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.5.0
|
||||
'@peculiar/asn1-pkcs8': 2.5.0
|
||||
'@peculiar/asn1-rsa': 2.5.0
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pkcs8@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
'@peculiar/asn1-x509': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-pkcs9@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.5.0
|
||||
'@peculiar/asn1-pfx': 2.5.0
|
||||
'@peculiar/asn1-pkcs8': 2.5.0
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
'@peculiar/asn1-x509': 2.5.0
|
||||
'@peculiar/asn1-x509-attr': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-rsa@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
'@peculiar/asn1-x509': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-schema@2.5.0':
|
||||
dependencies:
|
||||
asn1js: 3.0.6
|
||||
pvtsutils: 1.3.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-x509-attr@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
'@peculiar/asn1-x509': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/asn1-x509@2.5.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
asn1js: 3.0.6
|
||||
pvtsutils: 1.3.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@peculiar/x509@1.14.0':
|
||||
dependencies:
|
||||
'@peculiar/asn1-cms': 2.5.0
|
||||
'@peculiar/asn1-csr': 2.5.0
|
||||
'@peculiar/asn1-ecc': 2.5.0
|
||||
'@peculiar/asn1-pkcs9': 2.5.0
|
||||
'@peculiar/asn1-rsa': 2.5.0
|
||||
'@peculiar/asn1-schema': 2.5.0
|
||||
'@peculiar/asn1-x509': 2.5.0
|
||||
pvtsutils: 1.3.6
|
||||
reflect-metadata: 0.2.2
|
||||
tslib: 2.8.1
|
||||
tsyringe: 4.10.0
|
||||
|
||||
'@pnpm/config.env-replace@1.1.0': {}
|
||||
|
||||
'@pnpm/network.ca-file@1.0.2':
|
||||
@@ -5227,6 +5417,22 @@ snapshots:
|
||||
dependencies:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
|
||||
'@push.rocks/smartdns@7.6.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartenv': 5.0.13
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@tsclass/tsclass': 9.2.0
|
||||
'@types/dns-packet': 5.6.5
|
||||
'@types/elliptic': 6.4.18
|
||||
acme-client: 5.4.0
|
||||
dns-packet: 5.6.1
|
||||
elliptic: 6.6.1
|
||||
minimatch: 10.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartenv@5.0.13':
|
||||
dependencies:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@@ -5367,13 +5573,16 @@ snapshots:
|
||||
- socks
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartnetwork@4.2.0':
|
||||
'@push.rocks/smartnetwork@4.4.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartdns': 7.6.1
|
||||
'@push.rocks/smartping': 1.0.8
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartstring': 4.0.15
|
||||
isopen: 1.3.0
|
||||
systeminformation: 5.27.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartnpm@2.0.6':
|
||||
dependencies:
|
||||
@@ -5421,7 +5630,7 @@ snapshots:
|
||||
'@push.rocks/smartbuffer': 3.0.5
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.7
|
||||
'@push.rocks/smartnetwork': 4.2.0
|
||||
'@push.rocks/smartnetwork': 4.4.0
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.2)
|
||||
@@ -6282,6 +6491,10 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/bn.js@5.2.0':
|
||||
dependencies:
|
||||
'@types/node': 22.17.2
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
@@ -6306,6 +6519,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
dependencies:
|
||||
'@types/node': 22.17.2
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
dependencies:
|
||||
'@types/bn.js': 5.2.0
|
||||
|
||||
'@types/express-serve-static-core@5.0.7':
|
||||
dependencies:
|
||||
'@types/node': 22.17.2
|
||||
@@ -6446,6 +6667,16 @@ snapshots:
|
||||
mime-types: 3.0.1
|
||||
negotiator: 1.0.0
|
||||
|
||||
acme-client@5.4.0:
|
||||
dependencies:
|
||||
'@peculiar/x509': 1.14.0
|
||||
asn1js: 3.0.6
|
||||
axios: 1.12.1(debug@4.4.1)
|
||||
debug: 4.4.1
|
||||
node-forge: 1.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
agentkeepalive@4.6.0:
|
||||
@@ -6476,6 +6707,12 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
asn1js@3.0.6:
|
||||
dependencies:
|
||||
pvtsutils: 1.3.6
|
||||
pvutils: 1.1.3
|
||||
tslib: 2.8.1
|
||||
|
||||
ast-types@0.13.4:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -6488,6 +6725,14 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.12.1(debug@4.4.1):
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11(debug@4.4.1)
|
||||
form-data: 4.0.4
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
b4a@1.6.7: {}
|
||||
|
||||
bail@2.0.2: {}
|
||||
@@ -6525,6 +6770,8 @@ snapshots:
|
||||
|
||||
basic-ftp@5.0.5: {}
|
||||
|
||||
bn.js@4.12.2: {}
|
||||
|
||||
body-parser@2.2.0:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -6559,6 +6806,8 @@ snapshots:
|
||||
p-queue: 6.6.2
|
||||
unload: 2.4.1
|
||||
|
||||
brorand@1.1.0: {}
|
||||
|
||||
bson@6.10.4: {}
|
||||
|
||||
buffer-crc32@0.2.13: {}
|
||||
@@ -6820,6 +7069,10 @@ snapshots:
|
||||
dependencies:
|
||||
streamsearch: 0.1.2
|
||||
|
||||
dns-packet@5.6.1:
|
||||
dependencies:
|
||||
'@leichtgewicht/ip-codec': 2.0.5
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -6830,6 +7083,16 @@ snapshots:
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
elliptic@6.6.1:
|
||||
dependencies:
|
||||
bn.js: 4.12.2
|
||||
brorand: 1.1.0
|
||||
hash.js: 1.1.7
|
||||
hmac-drbg: 1.0.1
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
minimalistic-crypto-utils: 1.0.1
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
@@ -7236,6 +7499,11 @@ snapshots:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hash.js@1.1.7:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
@@ -7266,6 +7534,12 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hmac-drbg@1.0.1:
|
||||
dependencies:
|
||||
hash.js: 1.1.7
|
||||
minimalistic-assert: 1.0.1
|
||||
minimalistic-crypto-utils: 1.0.1
|
||||
|
||||
html-minifier@4.0.0:
|
||||
dependencies:
|
||||
camel-case: 3.0.0
|
||||
@@ -7945,6 +8219,10 @@ snapshots:
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
minimalistic-assert@1.0.1: {}
|
||||
|
||||
minimalistic-crypto-utils@1.0.1: {}
|
||||
|
||||
minimatch@10.0.3:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
@@ -8257,6 +8535,12 @@ snapshots:
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
pvtsutils@1.3.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
pvutils@1.1.3: {}
|
||||
|
||||
qs@6.14.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
@@ -8293,6 +8577,8 @@ snapshots:
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
regenerator-runtime@0.14.1: {}
|
||||
|
||||
registry-auth-token@5.1.0:
|
||||
@@ -8745,6 +9031,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
tsyringe@4.10.0:
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
|
||||
turndown-plugin-gfm@1.0.2: {}
|
||||
|
||||
turndown@7.2.1:
|
||||
|
111
test/test.migration.node.ts
Normal file
111
test/test.migration.node.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { expect, tap } from '../ts_tapbundle/index.js';
|
||||
import { Migration } from '../ts/tstest.classes.migration.js';
|
||||
import * as plugins from '../ts/tstest.plugins.js';
|
||||
import * as paths from '../ts/tstest.paths.js';
|
||||
|
||||
tap.test('Migration - can initialize', async () => {
|
||||
const migration = new Migration({
|
||||
baseDir: process.cwd(),
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
expect(migration).toBeInstanceOf(Migration);
|
||||
});
|
||||
|
||||
tap.test('Migration - findLegacyFiles returns empty for no legacy files', async () => {
|
||||
const migration = new Migration({
|
||||
baseDir: process.cwd(),
|
||||
pattern: 'test/test.migration.node.ts', // This file itself, not legacy
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
const legacyFiles = await migration.findLegacyFiles();
|
||||
expect(legacyFiles).toEqual([]);
|
||||
});
|
||||
|
||||
tap.test('Migration - generateReport works', async () => {
|
||||
const migration = new Migration({
|
||||
baseDir: process.cwd(),
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
const report = await migration.generateReport();
|
||||
expect(report).toBeTypeOf('string');
|
||||
expect(report).toContain('Test File Migration Report');
|
||||
});
|
||||
|
||||
tap.test('Migration - detects legacy files when they exist', async () => {
|
||||
// Create a temporary legacy test file
|
||||
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration');
|
||||
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
|
||||
|
||||
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
|
||||
await plugins.smartfile.memory.toFs('// Legacy test file\nexport default Promise.resolve();', legacyFile);
|
||||
|
||||
const migration = new Migration({
|
||||
baseDir: tempDir,
|
||||
pattern: '**/*.ts',
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
const legacyFiles = await migration.findLegacyFiles();
|
||||
expect(legacyFiles.length).toEqual(1);
|
||||
expect(legacyFiles[0]).toContain('test.browser.ts');
|
||||
|
||||
// Clean up
|
||||
await plugins.smartfile.fs.removeSync(tempDir);
|
||||
});
|
||||
|
||||
tap.test('Migration - detects both legacy pattern', async () => {
|
||||
// Create temporary legacy files
|
||||
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_both');
|
||||
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
|
||||
|
||||
const browserFile = plugins.path.join(tempDir, 'test.browser.ts');
|
||||
const bothFile = plugins.path.join(tempDir, 'test.both.ts');
|
||||
await plugins.smartfile.memory.toFs('// Browser test\nexport default Promise.resolve();', browserFile);
|
||||
await plugins.smartfile.memory.toFs('// Both test\nexport default Promise.resolve();', bothFile);
|
||||
|
||||
const migration = new Migration({
|
||||
baseDir: tempDir,
|
||||
pattern: '**/*.ts',
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
const legacyFiles = await migration.findLegacyFiles();
|
||||
expect(legacyFiles.length).toEqual(2);
|
||||
|
||||
// Clean up
|
||||
await plugins.smartfile.fs.removeSync(tempDir);
|
||||
});
|
||||
|
||||
tap.test('Migration - dry run does not modify files', async () => {
|
||||
// Create a temporary legacy test file
|
||||
const tempDir = plugins.path.join(process.cwd(), '.nogit', 'test_migration_dryrun');
|
||||
await plugins.smartfile.fs.ensureEmptyDir(tempDir);
|
||||
|
||||
const legacyFile = plugins.path.join(tempDir, 'test.browser.ts');
|
||||
await plugins.smartfile.memory.toFs('// Legacy test file\nexport default Promise.resolve();', legacyFile);
|
||||
|
||||
const migration = new Migration({
|
||||
baseDir: tempDir,
|
||||
pattern: '**/*.ts',
|
||||
dryRun: true,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
const summary = await migration.run();
|
||||
|
||||
expect(summary.dryRun).toEqual(true);
|
||||
expect(summary.totalLegacyFiles).toEqual(1);
|
||||
expect(summary.migratedCount).toEqual(1); // Dry run still counts as "would migrate"
|
||||
|
||||
// Verify original file still exists
|
||||
const fileExists = await plugins.smartfile.fs.fileExists(legacyFile);
|
||||
expect(fileExists).toEqual(true);
|
||||
|
||||
// Clean up
|
||||
await plugins.smartfile.fs.removeSync(tempDir);
|
||||
});
|
||||
|
||||
export default tap.start();
|
167
test/test.runtime.parser.node.ts
Normal file
167
test/test.runtime.parser.node.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { expect, tap } from '../ts_tapbundle/index.js';
|
||||
import { parseTestFilename, isLegacyFilename, getLegacyMigrationTarget } from '../ts/tstest.classes.runtime.parser.js';
|
||||
|
||||
tap.test('parseTestFilename - single runtime', async () => {
|
||||
const parsed = parseTestFilename('test.node.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['node']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - chromium runtime', async () => {
|
||||
const parsed = parseTestFilename('test.chromium.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['chromium']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - multiple runtimes', async () => {
|
||||
const parsed = parseTestFilename('test.node+chromium.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['node', 'chromium']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - deno+bun runtime', async () => {
|
||||
const parsed = parseTestFilename('test.deno+bun.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['deno', 'bun']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - with nonci modifier', async () => {
|
||||
const parsed = parseTestFilename('test.chromium.nonci.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['chromium']);
|
||||
expect(parsed.modifiers).toEqual(['nonci']);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - multi-runtime with nonci', async () => {
|
||||
const parsed = parseTestFilename('test.node+chromium.nonci.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['node', 'chromium']);
|
||||
expect(parsed.modifiers).toEqual(['nonci']);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - legacy browser', async () => {
|
||||
const parsed = parseTestFilename('test.browser.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['chromium']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - legacy both', async () => {
|
||||
const parsed = parseTestFilename('test.both.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['node', 'chromium']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - legacy browser with nonci', async () => {
|
||||
const parsed = parseTestFilename('test.browser.nonci.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['chromium']);
|
||||
expect(parsed.modifiers).toEqual(['nonci']);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - complex basename', async () => {
|
||||
const parsed = parseTestFilename('test.some.feature.node.ts');
|
||||
expect(parsed.baseName).toEqual('test.some.feature');
|
||||
expect(parsed.runtimes).toEqual(['node']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - default to node when no runtime', async () => {
|
||||
const parsed = parseTestFilename('test.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['node']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - tsx extension', async () => {
|
||||
const parsed = parseTestFilename('test.chromium.tsx');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['chromium']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('tsx');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - deduplicates runtime tokens', async () => {
|
||||
const parsed = parseTestFilename('test.node+node.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['node']);
|
||||
expect(parsed.modifiers).toEqual([]);
|
||||
expect(parsed.extension).toEqual('ts');
|
||||
expect(parsed.isLegacy).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('isLegacyFilename - detects browser', async () => {
|
||||
expect(isLegacyFilename('test.browser.ts')).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('isLegacyFilename - detects both', async () => {
|
||||
expect(isLegacyFilename('test.both.ts')).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('isLegacyFilename - rejects new naming', async () => {
|
||||
expect(isLegacyFilename('test.node.ts')).toEqual(false);
|
||||
expect(isLegacyFilename('test.chromium.ts')).toEqual(false);
|
||||
expect(isLegacyFilename('test.node+chromium.ts')).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('getLegacyMigrationTarget - browser to chromium', async () => {
|
||||
const target = getLegacyMigrationTarget('test.browser.ts');
|
||||
expect(target).toEqual('test.chromium.ts');
|
||||
});
|
||||
|
||||
tap.test('getLegacyMigrationTarget - both to node+chromium', async () => {
|
||||
const target = getLegacyMigrationTarget('test.both.ts');
|
||||
expect(target).toEqual('test.node+chromium.ts');
|
||||
});
|
||||
|
||||
tap.test('getLegacyMigrationTarget - browser with nonci', async () => {
|
||||
const target = getLegacyMigrationTarget('test.browser.nonci.ts');
|
||||
expect(target).toEqual('test.chromium.nonci.ts');
|
||||
});
|
||||
|
||||
tap.test('getLegacyMigrationTarget - both with nonci', async () => {
|
||||
const target = getLegacyMigrationTarget('test.both.nonci.ts');
|
||||
expect(target).toEqual('test.node+chromium.nonci.ts');
|
||||
});
|
||||
|
||||
tap.test('getLegacyMigrationTarget - returns null for non-legacy', async () => {
|
||||
const target = getLegacyMigrationTarget('test.node.ts');
|
||||
expect(target).toEqual(null);
|
||||
});
|
||||
|
||||
tap.test('parseTestFilename - handles full paths', async () => {
|
||||
const parsed = parseTestFilename('/path/to/test.node+chromium.ts');
|
||||
expect(parsed.baseName).toEqual('test');
|
||||
expect(parsed.runtimes).toEqual(['node', 'chromium']);
|
||||
expect(parsed.original).toEqual('test.node+chromium.ts');
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tstest',
|
||||
version: '2.3.7',
|
||||
version: '2.4.1',
|
||||
description: 'a test utility to run tests that match test/**/*.ts'
|
||||
}
|
||||
|
316
ts/tstest.classes.migration.ts
Normal file
316
ts/tstest.classes.migration.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import { parseTestFilename, getLegacyMigrationTarget, isLegacyFilename } from './tstest.classes.runtime.parser.js';
|
||||
|
||||
/**
|
||||
* Migration result for a single file
|
||||
*/
|
||||
export interface MigrationResult {
|
||||
/**
|
||||
* Original file path
|
||||
*/
|
||||
oldPath: string;
|
||||
|
||||
/**
|
||||
* New file path after migration
|
||||
*/
|
||||
newPath: string;
|
||||
|
||||
/**
|
||||
* Whether the migration was performed
|
||||
*/
|
||||
migrated: boolean;
|
||||
|
||||
/**
|
||||
* Error message if migration failed
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration summary
|
||||
*/
|
||||
export interface MigrationSummary {
|
||||
/**
|
||||
* Total number of legacy files found
|
||||
*/
|
||||
totalLegacyFiles: number;
|
||||
|
||||
/**
|
||||
* Number of files successfully migrated
|
||||
*/
|
||||
migratedCount: number;
|
||||
|
||||
/**
|
||||
* Number of files that failed to migrate
|
||||
*/
|
||||
errorCount: number;
|
||||
|
||||
/**
|
||||
* Individual migration results
|
||||
*/
|
||||
results: MigrationResult[];
|
||||
|
||||
/**
|
||||
* Whether this was a dry run
|
||||
*/
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration options
|
||||
*/
|
||||
export interface MigrationOptions {
|
||||
/**
|
||||
* Base directory to search for test files
|
||||
* Default: process.cwd()
|
||||
*/
|
||||
baseDir?: string;
|
||||
|
||||
/**
|
||||
* Glob pattern for finding test files
|
||||
* Default: '** /*test*.ts' (without space)
|
||||
*/
|
||||
pattern?: string;
|
||||
|
||||
/**
|
||||
* Dry run mode - don't actually rename files
|
||||
* Default: true
|
||||
*/
|
||||
dryRun?: boolean;
|
||||
|
||||
/**
|
||||
* Verbose output
|
||||
* Default: false
|
||||
*/
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration class for renaming legacy test files to new naming convention
|
||||
*
|
||||
* Migrations:
|
||||
* - .browser.ts → .chromium.ts
|
||||
* - .both.ts → .node+chromium.ts
|
||||
* - .both.nonci.ts → .node+chromium.nonci.ts
|
||||
* - .browser.nonci.ts → .chromium.nonci.ts
|
||||
*/
|
||||
export class Migration {
|
||||
private options: Required<MigrationOptions>;
|
||||
|
||||
constructor(options: MigrationOptions = {}) {
|
||||
this.options = {
|
||||
baseDir: options.baseDir || process.cwd(),
|
||||
pattern: options.pattern || '**/test*.ts',
|
||||
dryRun: options.dryRun !== undefined ? options.dryRun : true,
|
||||
verbose: options.verbose || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all legacy test files in the base directory
|
||||
*/
|
||||
async findLegacyFiles(): Promise<string[]> {
|
||||
const files = await plugins.smartfile.fs.listFileTree(
|
||||
this.options.baseDir,
|
||||
this.options.pattern
|
||||
);
|
||||
|
||||
const legacyFiles: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const fileName = plugins.path.basename(file);
|
||||
if (isLegacyFilename(fileName)) {
|
||||
const absolutePath = plugins.path.isAbsolute(file)
|
||||
? file
|
||||
: plugins.path.join(this.options.baseDir, file);
|
||||
legacyFiles.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
return legacyFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a single file
|
||||
*/
|
||||
private async migrateFile(filePath: string): Promise<MigrationResult> {
|
||||
const fileName = plugins.path.basename(filePath);
|
||||
const dirName = plugins.path.dirname(filePath);
|
||||
|
||||
try {
|
||||
// Get the new filename
|
||||
const newFileName = getLegacyMigrationTarget(fileName);
|
||||
|
||||
if (!newFileName) {
|
||||
return {
|
||||
oldPath: filePath,
|
||||
newPath: filePath,
|
||||
migrated: false,
|
||||
error: 'File is not a legacy file',
|
||||
};
|
||||
}
|
||||
|
||||
const newPath = plugins.path.join(dirName, newFileName);
|
||||
|
||||
// Check if target file already exists
|
||||
if (await plugins.smartfile.fs.fileExists(newPath)) {
|
||||
return {
|
||||
oldPath: filePath,
|
||||
newPath,
|
||||
migrated: false,
|
||||
error: `Target file already exists: ${newPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
// Check if we're in a git repository
|
||||
const isGitRepo = await this.isGitRepository(this.options.baseDir);
|
||||
|
||||
if (isGitRepo) {
|
||||
// Use git mv to preserve history
|
||||
const smartshell = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
pathDirectories: [],
|
||||
});
|
||||
const gitCommand = `cd "${this.options.baseDir}" && git mv "${filePath}" "${newPath}"`;
|
||||
const result = await smartshell.exec(gitCommand);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`git mv failed: ${result.stderr}`);
|
||||
}
|
||||
} else {
|
||||
// Not a git repository - cannot migrate without git
|
||||
throw new Error('Migration requires a git repository. We have git!');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oldPath: filePath,
|
||||
newPath,
|
||||
migrated: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
oldPath: filePath,
|
||||
newPath: filePath,
|
||||
migrated: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directory is a git repository
|
||||
*/
|
||||
private async isGitRepository(dir: string): Promise<boolean> {
|
||||
try {
|
||||
const gitDir = plugins.path.join(dir, '.git');
|
||||
return await plugins.smartfile.fs.isDirectory(gitDir);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
async run(): Promise<MigrationSummary> {
|
||||
const legacyFiles = await this.findLegacyFiles();
|
||||
|
||||
console.log('');
|
||||
console.log(cs('='.repeat(60), 'blue'));
|
||||
console.log(cs('Test File Migration Tool', 'blue'));
|
||||
console.log(cs('='.repeat(60), 'blue'));
|
||||
console.log('');
|
||||
|
||||
if (this.options.dryRun) {
|
||||
console.log(cs('🔍 DRY RUN MODE - No files will be modified', 'orange'));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`Found ${legacyFiles.length} legacy test file(s)`);
|
||||
console.log('');
|
||||
|
||||
const results: MigrationResult[] = [];
|
||||
let migratedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const file of legacyFiles) {
|
||||
const result = await this.migrateFile(file);
|
||||
results.push(result);
|
||||
|
||||
if (result.migrated) {
|
||||
migratedCount++;
|
||||
const oldName = plugins.path.basename(result.oldPath);
|
||||
const newName = plugins.path.basename(result.newPath);
|
||||
|
||||
if (this.options.dryRun) {
|
||||
console.log(cs(` Would migrate:`, 'cyan'));
|
||||
} else {
|
||||
console.log(cs(` ✓ Migrated:`, 'green'));
|
||||
}
|
||||
console.log(` ${oldName}`);
|
||||
console.log(cs(` → ${newName}`, 'green'));
|
||||
console.log('');
|
||||
} else if (result.error) {
|
||||
errorCount++;
|
||||
console.log(cs(` ✗ Failed: ${plugins.path.basename(result.oldPath)}`, 'red'));
|
||||
console.log(cs(` ${result.error}`, 'red'));
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(cs('='.repeat(60), 'blue'));
|
||||
console.log(`Summary:`);
|
||||
console.log(` Total legacy files: ${legacyFiles.length}`);
|
||||
console.log(` Successfully migrated: ${migratedCount}`);
|
||||
console.log(` Errors: ${errorCount}`);
|
||||
console.log(cs('='.repeat(60), 'blue'));
|
||||
|
||||
if (this.options.dryRun && legacyFiles.length > 0) {
|
||||
console.log('');
|
||||
console.log(cs('To apply these changes, run:', 'orange'));
|
||||
console.log(cs(' tstest migrate --write', 'orange'));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
return {
|
||||
totalLegacyFiles: legacyFiles.length,
|
||||
migratedCount,
|
||||
errorCount,
|
||||
results,
|
||||
dryRun: this.options.dryRun,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a migration report without performing the migration
|
||||
*/
|
||||
async generateReport(): Promise<string> {
|
||||
const legacyFiles = await this.findLegacyFiles();
|
||||
|
||||
let report = '';
|
||||
report += 'Test File Migration Report\n';
|
||||
report += '='.repeat(60) + '\n';
|
||||
report += '\n';
|
||||
report += `Found ${legacyFiles.length} legacy test file(s)\n`;
|
||||
report += '\n';
|
||||
|
||||
for (const file of legacyFiles) {
|
||||
const fileName = plugins.path.basename(file);
|
||||
const newFileName = getLegacyMigrationTarget(fileName);
|
||||
|
||||
if (newFileName) {
|
||||
report += `${fileName}\n`;
|
||||
report += ` → ${newFileName}\n`;
|
||||
report += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
report += '='.repeat(60) + '\n';
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
245
ts/tstest.classes.runtime.adapter.ts
Normal file
245
ts/tstest.classes.runtime.adapter.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||
|
||||
/**
|
||||
* Runtime-specific configuration options
|
||||
*/
|
||||
export interface RuntimeOptions {
|
||||
/**
|
||||
* Environment variables to pass to the runtime
|
||||
*/
|
||||
env?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Additional command-line arguments
|
||||
*/
|
||||
extraArgs?: string[];
|
||||
|
||||
/**
|
||||
* Working directory for test execution
|
||||
*/
|
||||
cwd?: string;
|
||||
|
||||
/**
|
||||
* Timeout in milliseconds (0 = no timeout)
|
||||
*/
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deno-specific configuration options
|
||||
*/
|
||||
export interface DenoOptions extends RuntimeOptions {
|
||||
/**
|
||||
* Permissions to grant to Deno
|
||||
* Default: ['--allow-read', '--allow-env']
|
||||
*/
|
||||
permissions?: string[];
|
||||
|
||||
/**
|
||||
* Path to deno.json config file
|
||||
*/
|
||||
configPath?: string;
|
||||
|
||||
/**
|
||||
* Path to import map file
|
||||
*/
|
||||
importMap?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chromium-specific configuration options
|
||||
*/
|
||||
export interface ChromiumOptions extends RuntimeOptions {
|
||||
/**
|
||||
* Chromium launch arguments
|
||||
*/
|
||||
launchArgs?: string[];
|
||||
|
||||
/**
|
||||
* Headless mode (default: true)
|
||||
*/
|
||||
headless?: boolean;
|
||||
|
||||
/**
|
||||
* Port range for HTTP server
|
||||
*/
|
||||
portRange?: { min: number; max: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Command configuration returned by createCommand()
|
||||
*/
|
||||
export interface RuntimeCommand {
|
||||
/**
|
||||
* The main command executable (e.g., 'node', 'deno', 'bun')
|
||||
*/
|
||||
command: string;
|
||||
|
||||
/**
|
||||
* Command-line arguments
|
||||
*/
|
||||
args: string[];
|
||||
|
||||
/**
|
||||
* Environment variables
|
||||
*/
|
||||
env?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Working directory
|
||||
*/
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime availability check result
|
||||
*/
|
||||
export interface RuntimeAvailability {
|
||||
/**
|
||||
* Whether the runtime is available
|
||||
*/
|
||||
available: boolean;
|
||||
|
||||
/**
|
||||
* Version string if available
|
||||
*/
|
||||
version?: string;
|
||||
|
||||
/**
|
||||
* Error message if not available
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for runtime adapters
|
||||
* Each runtime (Node, Chromium, Deno, Bun) implements this interface
|
||||
*/
|
||||
export abstract class RuntimeAdapter {
|
||||
/**
|
||||
* Runtime identifier
|
||||
*/
|
||||
abstract readonly id: Runtime;
|
||||
|
||||
/**
|
||||
* Human-readable display name
|
||||
*/
|
||||
abstract readonly displayName: string;
|
||||
|
||||
/**
|
||||
* Check if this runtime is available on the system
|
||||
* @returns Availability information including version
|
||||
*/
|
||||
abstract checkAvailable(): Promise<RuntimeAvailability>;
|
||||
|
||||
/**
|
||||
* Create the command configuration for executing a test
|
||||
* @param testFile - Absolute path to the test file
|
||||
* @param options - Runtime-specific options
|
||||
* @returns Command configuration
|
||||
*/
|
||||
abstract createCommand(testFile: string, options?: RuntimeOptions): RuntimeCommand;
|
||||
|
||||
/**
|
||||
* Execute a test file and return a TAP parser
|
||||
* @param testFile - Absolute path to the test file
|
||||
* @param index - Test index (for display)
|
||||
* @param total - Total number of tests (for display)
|
||||
* @param options - Runtime-specific options
|
||||
* @returns TAP parser with test results
|
||||
*/
|
||||
abstract run(
|
||||
testFile: string,
|
||||
index: number,
|
||||
total: number,
|
||||
options?: RuntimeOptions
|
||||
): Promise<TapParser>;
|
||||
|
||||
/**
|
||||
* Get the default options for this runtime
|
||||
* Can be overridden by subclasses
|
||||
*/
|
||||
protected getDefaultOptions(): RuntimeOptions {
|
||||
return {
|
||||
timeout: 0,
|
||||
extraArgs: [],
|
||||
env: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge user options with defaults
|
||||
*/
|
||||
protected mergeOptions<T extends RuntimeOptions>(userOptions?: T): T {
|
||||
const defaults = this.getDefaultOptions();
|
||||
return {
|
||||
...defaults,
|
||||
...userOptions,
|
||||
env: { ...defaults.env, ...userOptions?.env },
|
||||
extraArgs: [...(defaults.extraArgs || []), ...(userOptions?.extraArgs || [])],
|
||||
} as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for runtime adapters
|
||||
* Manages all available runtime implementations
|
||||
*/
|
||||
export class RuntimeAdapterRegistry {
|
||||
private adapters: Map<Runtime, RuntimeAdapter> = new Map();
|
||||
|
||||
/**
|
||||
* Register a runtime adapter
|
||||
*/
|
||||
register(adapter: RuntimeAdapter): void {
|
||||
this.adapters.set(adapter.id, adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an adapter by runtime ID
|
||||
*/
|
||||
get(runtime: Runtime): RuntimeAdapter | undefined {
|
||||
return this.adapters.get(runtime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered adapters
|
||||
*/
|
||||
getAll(): RuntimeAdapter[] {
|
||||
return Array.from(this.adapters.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which runtimes are available on the system
|
||||
*/
|
||||
async checkAvailability(): Promise<Map<Runtime, RuntimeAvailability>> {
|
||||
const results = new Map<Runtime, RuntimeAvailability>();
|
||||
|
||||
for (const [runtime, adapter] of this.adapters) {
|
||||
const availability = await adapter.checkAvailable();
|
||||
results.set(runtime, availability);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get adapters for a list of runtimes, in order
|
||||
* @param runtimes - Ordered list of runtimes
|
||||
* @returns Adapters in the same order, skipping any that aren't registered
|
||||
*/
|
||||
getAdaptersForRuntimes(runtimes: Runtime[]): RuntimeAdapter[] {
|
||||
const adapters: RuntimeAdapter[] = [];
|
||||
|
||||
for (const runtime of runtimes) {
|
||||
const adapter = this.get(runtime);
|
||||
if (adapter) {
|
||||
adapters.push(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
return adapters;
|
||||
}
|
||||
}
|
219
ts/tstest.classes.runtime.bun.ts
Normal file
219
ts/tstest.classes.runtime.bun.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import {
|
||||
RuntimeAdapter,
|
||||
type RuntimeOptions,
|
||||
type RuntimeCommand,
|
||||
type RuntimeAvailability,
|
||||
} from './tstest.classes.runtime.adapter.js';
|
||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
||||
|
||||
/**
|
||||
* Bun runtime adapter
|
||||
* Executes tests using the Bun runtime with native TypeScript support
|
||||
*/
|
||||
export class BunRuntimeAdapter extends RuntimeAdapter {
|
||||
readonly id: Runtime = 'bun';
|
||||
readonly displayName: string = 'Bun';
|
||||
|
||||
constructor(
|
||||
private logger: TsTestLogger,
|
||||
private smartshellInstance: any, // SmartShell instance from @push.rocks/smartshell
|
||||
private timeoutSeconds: number | null,
|
||||
private filterTags: string[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Bun is available
|
||||
*/
|
||||
async checkAvailable(): Promise<RuntimeAvailability> {
|
||||
try {
|
||||
const result = await this.smartshellInstance.exec('bun --version', {
|
||||
cwd: process.cwd(),
|
||||
onError: () => {
|
||||
// Ignore error
|
||||
}
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'Bun not found. Install from: https://bun.sh/',
|
||||
};
|
||||
}
|
||||
|
||||
// Bun version is just the version number
|
||||
const version = result.stdout.trim();
|
||||
|
||||
return {
|
||||
available: true,
|
||||
version: `Bun ${version}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create command configuration for Bun test execution
|
||||
*/
|
||||
createCommand(testFile: string, options?: RuntimeOptions): RuntimeCommand {
|
||||
const mergedOptions = this.mergeOptions(options);
|
||||
|
||||
const args: string[] = ['run'];
|
||||
|
||||
// Add extra args
|
||||
if (mergedOptions.extraArgs && mergedOptions.extraArgs.length > 0) {
|
||||
args.push(...mergedOptions.extraArgs);
|
||||
}
|
||||
|
||||
// Add test file
|
||||
args.push(testFile);
|
||||
|
||||
// Set environment variables
|
||||
const env = { ...mergedOptions.env };
|
||||
|
||||
if (this.filterTags.length > 0) {
|
||||
env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
}
|
||||
|
||||
return {
|
||||
command: 'bun',
|
||||
args,
|
||||
env,
|
||||
cwd: mergedOptions.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a test file in Bun
|
||||
*/
|
||||
async run(
|
||||
testFile: string,
|
||||
index: number,
|
||||
total: number,
|
||||
options?: RuntimeOptions
|
||||
): Promise<TapParser> {
|
||||
this.logger.testFileStart(testFile, this.displayName, index, total);
|
||||
const tapParser = new TapParser(testFile + ':bun', this.logger);
|
||||
|
||||
const mergedOptions = this.mergeOptions(options);
|
||||
|
||||
// Build Bun command
|
||||
const command = this.createCommand(testFile, mergedOptions);
|
||||
const fullCommand = `${command.command} ${command.args.join(' ')}`;
|
||||
|
||||
// Set filter tags as environment variable
|
||||
if (this.filterTags.length > 0) {
|
||||
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
}
|
||||
|
||||
// Check for 00init.ts file in test directory
|
||||
const testDir = plugins.path.dirname(testFile);
|
||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
||||
|
||||
let runCommand = fullCommand;
|
||||
let loaderPath: string | null = null;
|
||||
|
||||
// If 00init.ts exists, create a loader file
|
||||
if (initFileExists) {
|
||||
const absoluteInitFile = plugins.path.resolve(initFile);
|
||||
const absoluteTestFile = plugins.path.resolve(testFile);
|
||||
const loaderContent = `
|
||||
import '${absoluteInitFile.replace(/\\/g, '/')}';
|
||||
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||
`;
|
||||
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
||||
|
||||
// Rebuild command with loader file
|
||||
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
||||
runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`;
|
||||
}
|
||||
|
||||
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
||||
|
||||
// If we created a loader file, clean it up after test execution
|
||||
if (loaderPath) {
|
||||
const cleanup = () => {
|
||||
try {
|
||||
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
||||
plugins.smartfile.fs.removeSync(loaderPath);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
};
|
||||
|
||||
execResultStreaming.childProcess.on('exit', 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: ${testFile}`, '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
|
||||
if (this.timeoutSeconds !== null) {
|
||||
const timeoutMs = this.timeoutSeconds * 1000;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(async () => {
|
||||
// Use smartshell's terminate() to kill entire process tree
|
||||
await execResultStreaming.terminate();
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
clearTimeout(timeoutId);
|
||||
} catch (error) {
|
||||
// Clear warning timer if it was set
|
||||
if (warningTimer) {
|
||||
clearTimeout(warningTimer);
|
||||
}
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
// Ensure entire process tree is killed if still running
|
||||
try {
|
||||
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
||||
} catch (killError) {
|
||||
// Process tree might already be dead
|
||||
}
|
||||
await tapParser.evaluateFinalResult();
|
||||
}
|
||||
} else {
|
||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||
}
|
||||
|
||||
// Clear warning timer if it was set
|
||||
if (warningTimer) {
|
||||
clearTimeout(warningTimer);
|
||||
}
|
||||
|
||||
return tapParser;
|
||||
}
|
||||
}
|
293
ts/tstest.classes.runtime.chromium.ts
Normal file
293
ts/tstest.classes.runtime.chromium.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import * as paths from './tstest.paths.js';
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import {
|
||||
RuntimeAdapter,
|
||||
type ChromiumOptions,
|
||||
type RuntimeCommand,
|
||||
type RuntimeAvailability,
|
||||
} from './tstest.classes.runtime.adapter.js';
|
||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
||||
|
||||
/**
|
||||
* Chromium runtime adapter
|
||||
* Executes tests in a headless Chromium browser
|
||||
*/
|
||||
export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
||||
readonly id: Runtime = 'chromium';
|
||||
readonly displayName: string = 'Chromium';
|
||||
|
||||
constructor(
|
||||
private logger: TsTestLogger,
|
||||
private tsbundleInstance: any, // TsBundle instance from @push.rocks/tsbundle
|
||||
private smartbrowserInstance: any, // SmartBrowser instance from @push.rocks/smartbrowser
|
||||
private timeoutSeconds: number | null
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Chromium is available
|
||||
*/
|
||||
async checkAvailable(): Promise<RuntimeAvailability> {
|
||||
try {
|
||||
// Check if smartbrowser is available and can start
|
||||
// The browser binary is usually handled by @push.rocks/smartbrowser
|
||||
return {
|
||||
available: true,
|
||||
version: 'Chromium (via smartbrowser)',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: error.message || 'Chromium not available',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create command configuration for Chromium test execution
|
||||
* Note: Chromium tests don't use a traditional command, but this satisfies the interface
|
||||
*/
|
||||
createCommand(testFile: string, options?: ChromiumOptions): RuntimeCommand {
|
||||
const mergedOptions = this.mergeOptions(options);
|
||||
|
||||
return {
|
||||
command: 'chromium',
|
||||
args: [],
|
||||
env: mergedOptions.env,
|
||||
cwd: mergedOptions.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find free ports for HTTP server and WebSocket
|
||||
*/
|
||||
private async findFreePorts(): Promise<{ httpPort: number; wsPort: number }> {
|
||||
const smartnetwork = new plugins.smartnetwork.SmartNetwork();
|
||||
|
||||
// Find random free HTTP port in range 30000-40000 to minimize collision chance
|
||||
const httpPort = await smartnetwork.findFreePort(30000, 40000, { randomize: true });
|
||||
if (!httpPort) {
|
||||
throw new Error('Could not find a free HTTP port in range 30000-40000');
|
||||
}
|
||||
|
||||
// Find random free WebSocket port, excluding the HTTP port to ensure they're different
|
||||
const wsPort = await smartnetwork.findFreePort(30000, 40000, {
|
||||
randomize: true,
|
||||
exclude: [httpPort]
|
||||
});
|
||||
if (!wsPort) {
|
||||
throw new Error('Could not find a free WebSocket port in range 30000-40000');
|
||||
}
|
||||
|
||||
// Log selected ports for debugging
|
||||
if (!this.logger.options.quiet) {
|
||||
console.log(`Selected ports - HTTP: ${httpPort}, WebSocket: ${wsPort}`);
|
||||
}
|
||||
return { httpPort, wsPort };
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a test file in Chromium browser
|
||||
*/
|
||||
async run(
|
||||
testFile: string,
|
||||
index: number,
|
||||
total: number,
|
||||
options?: ChromiumOptions
|
||||
): Promise<TapParser> {
|
||||
this.logger.testFileStart(testFile, this.displayName, index, total);
|
||||
|
||||
// lets get all our paths sorted
|
||||
const tsbundleCacheDirPath = plugins.path.join(paths.cwd, './.nogit/tstest_cache');
|
||||
const bundleFileName = testFile.replace('/', '__') + '.js';
|
||||
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
|
||||
|
||||
// lets bundle the test
|
||||
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath);
|
||||
await this.tsbundleInstance.build(process.cwd(), testFile, bundleFilePath, {
|
||||
bundler: 'esbuild',
|
||||
});
|
||||
|
||||
// Find free ports for HTTP and WebSocket
|
||||
const { httpPort, wsPort } = await this.findFreePorts();
|
||||
|
||||
// lets create a server
|
||||
const server = new plugins.typedserver.servertools.Server({
|
||||
cors: true,
|
||||
port: httpPort,
|
||||
});
|
||||
server.addRoute(
|
||||
'/test',
|
||||
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
|
||||
res.type('.html');
|
||||
res.write(`
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
globalThis.testdom = true;
|
||||
globalThis.wsPort = ${wsPort};
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
|
||||
await server.start();
|
||||
|
||||
// lets handle realtime comms
|
||||
const tapParser = new TapParser(testFile + ':chrome', this.logger);
|
||||
const wss = new plugins.ws.WebSocketServer({ port: wsPort });
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('message', (message) => {
|
||||
const messageStr = message.toString();
|
||||
if (messageStr.startsWith('console:')) {
|
||||
const [, level, ...messageParts] = messageStr.split(':');
|
||||
this.logger.browserConsole(messageParts.join(':'), level);
|
||||
} else {
|
||||
tapParser.handleTapLog(messageStr);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// lets do the browser bit with timeout handling
|
||||
await this.smartbrowserInstance.start();
|
||||
|
||||
const evaluatePromise = this.smartbrowserInstance.evaluateOnPage(
|
||||
`http://localhost:${httpPort}/test?bundleName=${bundleFileName}`,
|
||||
async () => {
|
||||
// lets enable real time comms
|
||||
const ws = new WebSocket(`ws://localhost:${globalThis.wsPort}`);
|
||||
await new Promise((resolve) => (ws.onopen = resolve));
|
||||
|
||||
// Ensure this function is declared with 'async'
|
||||
const logStore = [];
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
|
||||
// Override console methods to capture the logs
|
||||
console.log = (...args: any[]) => {
|
||||
logStore.push(args.join(' '));
|
||||
ws.send(args.join(' '));
|
||||
originalLog(...args);
|
||||
};
|
||||
console.error = (...args: any[]) => {
|
||||
logStore.push(args.join(' '));
|
||||
ws.send(args.join(' '));
|
||||
originalError(...args);
|
||||
};
|
||||
|
||||
const bundleName = new URLSearchParams(window.location.search).get('bundleName');
|
||||
originalLog(`::TSTEST IN CHROMIUM:: Relevant Script name is: ${bundleName}`);
|
||||
|
||||
try {
|
||||
// Dynamically import the test module
|
||||
const testModule = await import(`/${bundleName}`);
|
||||
if (testModule && testModule.default && testModule.default instanceof Promise) {
|
||||
// Execute the exported test function
|
||||
await testModule.default;
|
||||
} else if (testModule && testModule.default && typeof testModule.default.then === 'function') {
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
console.log('Test module default export is just promiselike: Something might be messing with your Promise implementation.');
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
await testModule.default;
|
||||
} else if (globalThis.tapPromise && typeof globalThis.tapPromise.then === 'function') {
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
console.log('Using globalThis.tapPromise');
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
await testModule.default;
|
||||
} else {
|
||||
console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
console.error('Test module does not export a default promise.');
|
||||
console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
console.log(`We got: ${JSON.stringify(testModule)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return logStore.join('\n');
|
||||
}
|
||||
);
|
||||
|
||||
// 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: ${testFile}`, '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
|
||||
if (this.timeoutSeconds !== null) {
|
||||
const timeoutMs = this.timeoutSeconds * 1000;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
evaluatePromise,
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
clearTimeout(timeoutId);
|
||||
} catch (error) {
|
||||
// Clear warning timer if it was set
|
||||
if (warningTimer) {
|
||||
clearTimeout(warningTimer);
|
||||
}
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
}
|
||||
} else {
|
||||
await evaluatePromise;
|
||||
}
|
||||
|
||||
// Clear warning timer if it was set
|
||||
if (warningTimer) {
|
||||
clearTimeout(warningTimer);
|
||||
}
|
||||
|
||||
// Always clean up resources, even on timeout
|
||||
try {
|
||||
await this.smartbrowserInstance.stop();
|
||||
} catch (error) {
|
||||
// Browser might already be stopped
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (error) {
|
||||
// Server might already be stopped
|
||||
}
|
||||
|
||||
try {
|
||||
wss.close();
|
||||
} catch (error) {
|
||||
// WebSocket server might already be closed
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${cs('=> ', 'blue')} Stopped ${cs(testFile, 'orange')} chromium instance and server.`
|
||||
);
|
||||
// Always evaluate final result (handleTimeout just sets up the test state)
|
||||
await tapParser.evaluateFinalResult();
|
||||
return tapParser;
|
||||
}
|
||||
}
|
256
ts/tstest.classes.runtime.deno.ts
Normal file
256
ts/tstest.classes.runtime.deno.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import {
|
||||
RuntimeAdapter,
|
||||
type DenoOptions,
|
||||
type RuntimeCommand,
|
||||
type RuntimeAvailability,
|
||||
} from './tstest.classes.runtime.adapter.js';
|
||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
||||
|
||||
/**
|
||||
* Deno runtime adapter
|
||||
* Executes tests using the Deno runtime
|
||||
*/
|
||||
export class DenoRuntimeAdapter extends RuntimeAdapter {
|
||||
readonly id: Runtime = 'deno';
|
||||
readonly displayName: string = 'Deno';
|
||||
|
||||
constructor(
|
||||
private logger: TsTestLogger,
|
||||
private smartshellInstance: any, // SmartShell instance from @push.rocks/smartshell
|
||||
private timeoutSeconds: number | null,
|
||||
private filterTags: string[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default Deno options
|
||||
*/
|
||||
protected getDefaultOptions(): DenoOptions {
|
||||
return {
|
||||
...super.getDefaultOptions(),
|
||||
permissions: [
|
||||
'--allow-read',
|
||||
'--allow-env',
|
||||
'--allow-net',
|
||||
'--allow-write',
|
||||
'--sloppy-imports', // Allow .js imports to resolve to .ts files
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Deno is available
|
||||
*/
|
||||
async checkAvailable(): Promise<RuntimeAvailability> {
|
||||
try {
|
||||
const result = await this.smartshellInstance.exec('deno --version', {
|
||||
cwd: process.cwd(),
|
||||
onError: () => {
|
||||
// Ignore error
|
||||
}
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'Deno not found. Install from: https://deno.land/',
|
||||
};
|
||||
}
|
||||
|
||||
// Parse Deno version from output (first line is "deno X.Y.Z")
|
||||
const versionMatch = result.stdout.match(/deno (\d+\.\d+\.\d+)/);
|
||||
const version = versionMatch ? versionMatch[1] : 'unknown';
|
||||
|
||||
return {
|
||||
available: true,
|
||||
version: `Deno ${version}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create command configuration for Deno test execution
|
||||
*/
|
||||
createCommand(testFile: string, options?: DenoOptions): RuntimeCommand {
|
||||
const mergedOptions = this.mergeOptions(options) as DenoOptions;
|
||||
|
||||
const args: string[] = ['run'];
|
||||
|
||||
// Add permissions
|
||||
const permissions = mergedOptions.permissions || [
|
||||
'--allow-read',
|
||||
'--allow-env',
|
||||
'--allow-net',
|
||||
'--allow-write',
|
||||
'--sloppy-imports',
|
||||
];
|
||||
args.push(...permissions);
|
||||
|
||||
// Add config file if specified
|
||||
if (mergedOptions.configPath) {
|
||||
args.push('--config', mergedOptions.configPath);
|
||||
}
|
||||
|
||||
// Add import map if specified
|
||||
if (mergedOptions.importMap) {
|
||||
args.push('--import-map', mergedOptions.importMap);
|
||||
}
|
||||
|
||||
// Add extra args
|
||||
if (mergedOptions.extraArgs && mergedOptions.extraArgs.length > 0) {
|
||||
args.push(...mergedOptions.extraArgs);
|
||||
}
|
||||
|
||||
// Add test file
|
||||
args.push(testFile);
|
||||
|
||||
// Set environment variables
|
||||
const env = { ...mergedOptions.env };
|
||||
|
||||
if (this.filterTags.length > 0) {
|
||||
env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
}
|
||||
|
||||
return {
|
||||
command: 'deno',
|
||||
args,
|
||||
env,
|
||||
cwd: mergedOptions.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a test file in Deno
|
||||
*/
|
||||
async run(
|
||||
testFile: string,
|
||||
index: number,
|
||||
total: number,
|
||||
options?: DenoOptions
|
||||
): Promise<TapParser> {
|
||||
this.logger.testFileStart(testFile, this.displayName, index, total);
|
||||
const tapParser = new TapParser(testFile + ':deno', this.logger);
|
||||
|
||||
const mergedOptions = this.mergeOptions(options) as DenoOptions;
|
||||
|
||||
// Build Deno command
|
||||
const command = this.createCommand(testFile, mergedOptions);
|
||||
const fullCommand = `${command.command} ${command.args.join(' ')}`;
|
||||
|
||||
// Set filter tags as environment variable
|
||||
if (this.filterTags.length > 0) {
|
||||
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
}
|
||||
|
||||
// Check for 00init.ts file in test directory
|
||||
const testDir = plugins.path.dirname(testFile);
|
||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
||||
|
||||
let runCommand = fullCommand;
|
||||
let loaderPath: string | null = null;
|
||||
|
||||
// If 00init.ts exists, create a loader file
|
||||
if (initFileExists) {
|
||||
const absoluteInitFile = plugins.path.resolve(initFile);
|
||||
const absoluteTestFile = plugins.path.resolve(testFile);
|
||||
const loaderContent = `
|
||||
import '${absoluteInitFile.replace(/\\/g, '/')}';
|
||||
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||
`;
|
||||
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
||||
|
||||
// Rebuild command with loader file
|
||||
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
||||
runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`;
|
||||
}
|
||||
|
||||
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
||||
|
||||
// If we created a loader file, clean it up after test execution
|
||||
if (loaderPath) {
|
||||
const cleanup = () => {
|
||||
try {
|
||||
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
||||
plugins.smartfile.fs.removeSync(loaderPath);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
};
|
||||
|
||||
execResultStreaming.childProcess.on('exit', 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: ${testFile}`, '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
|
||||
if (this.timeoutSeconds !== null) {
|
||||
const timeoutMs = this.timeoutSeconds * 1000;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(async () => {
|
||||
// Use smartshell's terminate() to kill entire process tree
|
||||
await execResultStreaming.terminate();
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
clearTimeout(timeoutId);
|
||||
} catch (error) {
|
||||
// Clear warning timer if it was set
|
||||
if (warningTimer) {
|
||||
clearTimeout(warningTimer);
|
||||
}
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
// Ensure entire process tree is killed if still running
|
||||
try {
|
||||
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
||||
} catch (killError) {
|
||||
// Process tree might already be dead
|
||||
}
|
||||
await tapParser.evaluateFinalResult();
|
||||
}
|
||||
} else {
|
||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||
}
|
||||
|
||||
// Clear warning timer if it was set
|
||||
if (warningTimer) {
|
||||
clearTimeout(warningTimer);
|
||||
}
|
||||
|
||||
return tapParser;
|
||||
}
|
||||
}
|
222
ts/tstest.classes.runtime.node.ts
Normal file
222
ts/tstest.classes.runtime.node.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import {
|
||||
RuntimeAdapter,
|
||||
type RuntimeOptions,
|
||||
type RuntimeCommand,
|
||||
type RuntimeAvailability,
|
||||
} from './tstest.classes.runtime.adapter.js';
|
||||
import { TapParser } from './tstest.classes.tap.parser.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
||||
|
||||
/**
|
||||
* Node.js runtime adapter
|
||||
* Executes tests using tsrun (TypeScript runner for Node.js)
|
||||
*/
|
||||
export class NodeRuntimeAdapter extends RuntimeAdapter {
|
||||
readonly id: Runtime = 'node';
|
||||
readonly displayName: string = 'Node.js';
|
||||
|
||||
constructor(
|
||||
private logger: TsTestLogger,
|
||||
private smartshellInstance: any, // SmartShell instance from @push.rocks/smartshell
|
||||
private timeoutSeconds: number | null,
|
||||
private filterTags: string[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Node.js and tsrun are available
|
||||
*/
|
||||
async checkAvailable(): Promise<RuntimeAvailability> {
|
||||
try {
|
||||
// Check Node.js version
|
||||
const nodeVersion = process.version;
|
||||
|
||||
// Check if tsrun is available
|
||||
const result = await this.smartshellInstance.exec('tsrun --version', {
|
||||
cwd: process.cwd(),
|
||||
onError: () => {
|
||||
// Ignore error
|
||||
}
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'tsrun not found. Install with: pnpm install --save-dev @git.zone/tsrun',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
available: true,
|
||||
version: nodeVersion,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create command configuration for Node.js test execution
|
||||
*/
|
||||
createCommand(testFile: string, options?: RuntimeOptions): RuntimeCommand {
|
||||
const mergedOptions = this.mergeOptions(options);
|
||||
|
||||
// Build tsrun options
|
||||
const args: string[] = [];
|
||||
|
||||
if (process.argv.includes('--web')) {
|
||||
args.push('--web');
|
||||
}
|
||||
|
||||
// Add any extra args
|
||||
if (mergedOptions.extraArgs) {
|
||||
args.push(...mergedOptions.extraArgs);
|
||||
}
|
||||
|
||||
// Set environment variables
|
||||
const env = { ...mergedOptions.env };
|
||||
|
||||
if (this.filterTags.length > 0) {
|
||||
env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
}
|
||||
|
||||
return {
|
||||
command: 'tsrun',
|
||||
args: [testFile, ...args],
|
||||
env,
|
||||
cwd: mergedOptions.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a test file in Node.js
|
||||
*/
|
||||
async run(
|
||||
testFile: string,
|
||||
index: number,
|
||||
total: number,
|
||||
options?: RuntimeOptions
|
||||
): Promise<TapParser> {
|
||||
this.logger.testFileStart(testFile, this.displayName, index, total);
|
||||
const tapParser = new TapParser(testFile + ':node', this.logger);
|
||||
|
||||
const mergedOptions = this.mergeOptions(options);
|
||||
|
||||
// Build tsrun command
|
||||
let tsrunOptions = '';
|
||||
if (process.argv.includes('--web')) {
|
||||
tsrunOptions += ' --web';
|
||||
}
|
||||
|
||||
// Set filter tags as environment variable
|
||||
if (this.filterTags.length > 0) {
|
||||
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||
}
|
||||
|
||||
// Check for 00init.ts file in test directory
|
||||
const testDir = plugins.path.dirname(testFile);
|
||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||
let runCommand = `tsrun ${testFile}${tsrunOptions}`;
|
||||
|
||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
||||
|
||||
// If 00init.ts exists, run it first
|
||||
let loaderPath: string | null = null;
|
||||
if (initFileExists) {
|
||||
// Create a temporary loader file that imports both 00init.ts and the test file
|
||||
const absoluteInitFile = plugins.path.resolve(initFile);
|
||||
const absoluteTestFile = plugins.path.resolve(testFile);
|
||||
const loaderContent = `
|
||||
import '${absoluteInitFile.replace(/\\/g, '/')}';
|
||||
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||
`;
|
||||
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
||||
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
|
||||
}
|
||||
|
||||
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
||||
|
||||
// If we created a loader file, clean it up after test execution
|
||||
if (loaderPath) {
|
||||
const cleanup = () => {
|
||||
try {
|
||||
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
||||
plugins.smartfile.fs.removeSync(loaderPath);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
};
|
||||
|
||||
execResultStreaming.childProcess.on('exit', 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: ${testFile}`, '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
|
||||
if (this.timeoutSeconds !== null) {
|
||||
const timeoutMs = this.timeoutSeconds * 1000;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(async () => {
|
||||
// Use smartshell's terminate() to kill entire process tree
|
||||
await execResultStreaming.terminate();
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
clearTimeout(timeoutId);
|
||||
} catch (error) {
|
||||
// Clear warning timer if it was set
|
||||
if (warningTimer) {
|
||||
clearTimeout(warningTimer);
|
||||
}
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
// Ensure entire process tree is killed if still running
|
||||
try {
|
||||
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
||||
} catch (killError) {
|
||||
// Process tree might already be dead
|
||||
}
|
||||
await tapParser.evaluateFinalResult();
|
||||
}
|
||||
} else {
|
||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||
}
|
||||
|
||||
// Clear warning timer if it was set
|
||||
if (warningTimer) {
|
||||
clearTimeout(warningTimer);
|
||||
}
|
||||
|
||||
return tapParser;
|
||||
}
|
||||
}
|
211
ts/tstest.classes.runtime.parser.ts
Normal file
211
ts/tstest.classes.runtime.parser.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Runtime parser for test file naming convention
|
||||
* Supports: test.runtime1+runtime2.modifier.ts
|
||||
* Examples:
|
||||
* - test.node.ts
|
||||
* - test.chromium.ts
|
||||
* - test.node+chromium.ts
|
||||
* - test.deno+bun.ts
|
||||
* - test.chromium.nonci.ts
|
||||
*/
|
||||
|
||||
export type Runtime = 'node' | 'chromium' | 'deno' | 'bun';
|
||||
export type Modifier = 'nonci';
|
||||
|
||||
export interface ParsedFilename {
|
||||
baseName: string;
|
||||
runtimes: Runtime[];
|
||||
modifiers: Modifier[];
|
||||
extension: string;
|
||||
isLegacy: boolean;
|
||||
original: string;
|
||||
}
|
||||
|
||||
export interface ParserConfig {
|
||||
strictUnknownRuntime?: boolean; // default: true
|
||||
defaultRuntimes?: Runtime[]; // default: ['node']
|
||||
}
|
||||
|
||||
const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']);
|
||||
const KNOWN_MODIFIERS: Set<string> = new Set(['nonci']);
|
||||
const VALID_EXTENSIONS: Set<string> = new Set(['ts', 'tsx', 'mts', 'cts']);
|
||||
|
||||
// Legacy mappings for backwards compatibility
|
||||
const LEGACY_RUNTIME_MAP: Record<string, Runtime[]> = {
|
||||
browser: ['chromium'],
|
||||
both: ['node', 'chromium'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a test filename to extract runtimes, modifiers, and detect legacy patterns
|
||||
* Algorithm: Right-to-left token analysis from the extension
|
||||
*/
|
||||
export function parseTestFilename(
|
||||
filePath: string,
|
||||
config: ParserConfig = {}
|
||||
): ParsedFilename {
|
||||
const strictUnknownRuntime = config.strictUnknownRuntime ?? true;
|
||||
const defaultRuntimes = config.defaultRuntimes ?? ['node'];
|
||||
|
||||
// Extract just the filename from the path
|
||||
const fileName = filePath.split('/').pop() || filePath;
|
||||
const original = fileName;
|
||||
|
||||
// Step 1: Extract and validate extension
|
||||
const lastDot = fileName.lastIndexOf('.');
|
||||
if (lastDot === -1) {
|
||||
throw new Error(`Invalid test file: no extension found in "${fileName}"`);
|
||||
}
|
||||
|
||||
const extension = fileName.substring(lastDot + 1);
|
||||
if (!VALID_EXTENSIONS.has(extension)) {
|
||||
throw new Error(
|
||||
`Invalid test file extension ".${extension}" in "${fileName}". ` +
|
||||
`Valid extensions: ${Array.from(VALID_EXTENSIONS).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Split remaining basename by dots
|
||||
const withoutExtension = fileName.substring(0, lastDot);
|
||||
const tokens = withoutExtension.split('.');
|
||||
|
||||
if (tokens.length === 0) {
|
||||
throw new Error(`Invalid test file: empty basename in "${fileName}"`);
|
||||
}
|
||||
|
||||
// Step 3: Parse from right to left
|
||||
let isLegacy = false;
|
||||
const modifiers: Modifier[] = [];
|
||||
let runtimes: Runtime[] = [];
|
||||
let runtimeTokenIndex = -1;
|
||||
|
||||
// Scan from right to left
|
||||
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||
const token = tokens[i];
|
||||
|
||||
// Check if this is a known modifier
|
||||
if (KNOWN_MODIFIERS.has(token)) {
|
||||
modifiers.unshift(token as Modifier);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a legacy runtime token
|
||||
if (LEGACY_RUNTIME_MAP[token]) {
|
||||
isLegacy = true;
|
||||
runtimes = LEGACY_RUNTIME_MAP[token];
|
||||
runtimeTokenIndex = i;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if this is a runtime chain (may contain + separators)
|
||||
if (token.includes('+')) {
|
||||
const runtimeCandidates = token.split('+').map(r => r.trim()).filter(Boolean);
|
||||
const validRuntimes: Runtime[] = [];
|
||||
const invalidRuntimes: string[] = [];
|
||||
|
||||
for (const candidate of runtimeCandidates) {
|
||||
if (KNOWN_RUNTIMES.has(candidate)) {
|
||||
// Dedupe: only add if not already in list
|
||||
if (!validRuntimes.includes(candidate as Runtime)) {
|
||||
validRuntimes.push(candidate as Runtime);
|
||||
}
|
||||
} else {
|
||||
invalidRuntimes.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidRuntimes.length > 0) {
|
||||
if (strictUnknownRuntime) {
|
||||
throw new Error(
|
||||
`Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
||||
`Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}`
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`⚠️ Warning: Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
||||
`Defaulting to: ${defaultRuntimes.join('+')}`
|
||||
);
|
||||
runtimes = [...defaultRuntimes];
|
||||
runtimeTokenIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (validRuntimes.length > 0) {
|
||||
runtimes = validRuntimes;
|
||||
runtimeTokenIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a single runtime token
|
||||
if (KNOWN_RUNTIMES.has(token)) {
|
||||
runtimes = [token as Runtime];
|
||||
runtimeTokenIndex = i;
|
||||
break;
|
||||
}
|
||||
|
||||
// If we've scanned past modifiers and haven't found a runtime, stop looking
|
||||
if (modifiers.length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Determine base name
|
||||
// Everything before the runtime token (if found) is the base name
|
||||
const baseNameTokens = runtimeTokenIndex >= 0 ? tokens.slice(0, runtimeTokenIndex) : tokens;
|
||||
const baseName = baseNameTokens.join('.');
|
||||
|
||||
// Step 5: Apply defaults if no runtime was detected
|
||||
if (runtimes.length === 0) {
|
||||
runtimes = [...defaultRuntimes];
|
||||
}
|
||||
|
||||
return {
|
||||
baseName: baseName || 'test',
|
||||
runtimes,
|
||||
modifiers,
|
||||
extension,
|
||||
isLegacy,
|
||||
original,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a filename uses legacy naming convention
|
||||
*/
|
||||
export function isLegacyFilename(fileName: string): boolean {
|
||||
const tokens = fileName.split('.');
|
||||
for (const token of tokens) {
|
||||
if (LEGACY_RUNTIME_MAP[token]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the suggested new filename for a legacy filename
|
||||
*/
|
||||
export function getLegacyMigrationTarget(fileName: string): string | null {
|
||||
const parsed = parseTestFilename(fileName, { strictUnknownRuntime: false });
|
||||
|
||||
if (!parsed.isLegacy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reconstruct filename with new naming
|
||||
const parts = [parsed.baseName];
|
||||
|
||||
if (parsed.runtimes.length > 0) {
|
||||
parts.push(parsed.runtimes.join('+'));
|
||||
}
|
||||
|
||||
if (parsed.modifiers.length > 0) {
|
||||
parts.push(...parsed.modifiers);
|
||||
}
|
||||
|
||||
parts.push(parsed.extension);
|
||||
|
||||
return parts.join('.');
|
||||
}
|
@@ -10,6 +10,14 @@ import { TestExecutionMode } from './index.js';
|
||||
import { TsTestLogger } from './tstest.logging.js';
|
||||
import type { LogOptions } from './tstest.logging.js';
|
||||
|
||||
// Runtime adapters
|
||||
import { parseTestFilename } from './tstest.classes.runtime.parser.js';
|
||||
import { RuntimeAdapterRegistry } from './tstest.classes.runtime.adapter.js';
|
||||
import { NodeRuntimeAdapter } from './tstest.classes.runtime.node.js';
|
||||
import { ChromiumRuntimeAdapter } from './tstest.classes.runtime.chromium.js';
|
||||
import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js';
|
||||
import { BunRuntimeAdapter } from './tstest.classes.runtime.bun.js';
|
||||
|
||||
export class TsTest {
|
||||
public testDir: TestDirectory;
|
||||
public executionMode: TestExecutionMode;
|
||||
@@ -28,6 +36,8 @@ export class TsTest {
|
||||
|
||||
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
||||
|
||||
public runtimeRegistry = new RuntimeAdapterRegistry();
|
||||
|
||||
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null, timeoutSeconds: number | null = null) {
|
||||
this.executionMode = executionModeArg;
|
||||
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
||||
@@ -36,6 +46,20 @@ export class TsTest {
|
||||
this.startFromFile = startFromFile;
|
||||
this.stopAtFile = stopAtFile;
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
|
||||
// Register runtime adapters
|
||||
this.runtimeRegistry.register(
|
||||
new NodeRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags)
|
||||
);
|
||||
this.runtimeRegistry.register(
|
||||
new ChromiumRuntimeAdapter(this.logger, this.tsbundleInstance, this.smartbrowserInstance, this.timeoutSeconds)
|
||||
);
|
||||
this.runtimeRegistry.register(
|
||||
new DenoRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags)
|
||||
);
|
||||
this.runtimeRegistry.register(
|
||||
new BunRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags)
|
||||
);
|
||||
}
|
||||
|
||||
async run() {
|
||||
@@ -175,29 +199,50 @@ export class TsTest {
|
||||
}
|
||||
|
||||
private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
||||
switch (true) {
|
||||
case process.env.CI && fileNameArg.includes('.nonci.'):
|
||||
this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
|
||||
break;
|
||||
case fileNameArg.endsWith('.browser.ts') || fileNameArg.endsWith('.browser.nonci.ts'):
|
||||
const tapParserBrowser = await this.runInChrome(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParserBrowser);
|
||||
break;
|
||||
case fileNameArg.endsWith('.both.ts') || fileNameArg.endsWith('.both.nonci.ts'):
|
||||
this.logger.sectionStart('Part 1: Chrome');
|
||||
const tapParserBothBrowser = await this.runInChrome(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParserBothBrowser);
|
||||
this.logger.sectionEnd();
|
||||
// Parse the filename to determine runtimes and modifiers
|
||||
const fileName = plugins.path.basename(fileNameArg);
|
||||
const parsed = parseTestFilename(fileName, { strictUnknownRuntime: false });
|
||||
|
||||
this.logger.sectionStart('Part 2: Node');
|
||||
const tapParserBothNode = await this.runInNode(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParserBothNode);
|
||||
// Check for nonci modifier in CI environment
|
||||
if (process.env.CI && parsed.modifiers.includes('nonci')) {
|
||||
this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show deprecation warning for legacy naming
|
||||
if (parsed.isLegacy) {
|
||||
console.warn('');
|
||||
console.warn(cs('⚠️ DEPRECATION WARNING', 'orange'));
|
||||
console.warn(cs(` File: ${fileName}`, 'orange'));
|
||||
console.warn(cs(` Legacy naming detected. Please migrate to new naming convention.`, 'orange'));
|
||||
console.warn(cs(` Suggested: ${fileName.replace('.browser.', '.chromium.').replace('.both.', '.node+chromium.')}`, 'green'));
|
||||
console.warn(cs(` Run: tstest migrate --dry-run`, 'cyan'));
|
||||
console.warn('');
|
||||
}
|
||||
|
||||
// Get adapters for the specified runtimes
|
||||
const adapters = this.runtimeRegistry.getAdaptersForRuntimes(parsed.runtimes);
|
||||
|
||||
if (adapters.length === 0) {
|
||||
this.logger.tapOutput(`Skipping ${fileNameArg} - no runtime adapters available`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute tests for each runtime
|
||||
if (adapters.length === 1) {
|
||||
// Single runtime - no sections needed
|
||||
const adapter = adapters[0];
|
||||
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParser);
|
||||
} else {
|
||||
// Multiple runtimes - use sections
|
||||
for (let i = 0; i < adapters.length; i++) {
|
||||
const adapter = adapters[i];
|
||||
this.logger.sectionStart(`Part ${i + 1}: ${adapter.displayName}`);
|
||||
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParser);
|
||||
this.logger.sectionEnd();
|
||||
break;
|
||||
default:
|
||||
const tapParserNode = await this.runInNode(fileNameArg, fileIndex, totalFiles);
|
||||
tapCombinator.addTapParser(tapParserNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,20 +364,19 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
||||
private async findFreePorts(): Promise<{ httpPort: number; wsPort: number }> {
|
||||
const smartnetwork = new plugins.smartnetwork.SmartNetwork();
|
||||
|
||||
// Find HTTP port in range 30000-40000
|
||||
const httpPort = await smartnetwork.findFreePort(30000, 40000);
|
||||
// Find random free HTTP port in range 30000-40000 to minimize collision chance
|
||||
const httpPort = await smartnetwork.findFreePort(30000, 40000, { randomize: true });
|
||||
if (!httpPort) {
|
||||
throw new Error('Could not find a free HTTP port in range 30000-40000');
|
||||
}
|
||||
|
||||
// Find WebSocket port in range 30000-40000 (different from HTTP port)
|
||||
let wsPort = await smartnetwork.findFreePort(httpPort + 1, 40000);
|
||||
// Find random free WebSocket port, excluding the HTTP port to ensure they're different
|
||||
const wsPort = await smartnetwork.findFreePort(30000, 40000, {
|
||||
randomize: true,
|
||||
exclude: [httpPort]
|
||||
});
|
||||
if (!wsPort) {
|
||||
// Try again from the beginning of the range if we couldn't find one after httpPort
|
||||
wsPort = await smartnetwork.findFreePort(30000, httpPort - 1);
|
||||
if (!wsPort) {
|
||||
throw new Error('Could not find a free WebSocket port in range 30000-40000');
|
||||
}
|
||||
throw new Error('Could not find a free WebSocket port in range 30000-40000');
|
||||
}
|
||||
|
||||
// Log selected ports for debugging
|
||||
|
Reference in New Issue
Block a user