Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate tests to nodejs runner #481

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,480 changes: 1,013 additions & 4,467 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 4 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
"author": "SAP SE (https://www.sap.com)",
"license": "Apache-2.0",
"scripts": {
"test:unit": "jest --projects test/unit.jest.config.js",
"test:integration": "jest --projects test/int.jest.config.js",
"test:smoke": "jest --projects test/smoke.jest.config.js",
"test:all": "jest",
"test:unit": "node test/testRunner.js ./test/unit ./test/unit/setup.mjs",
"test:integration": "node test/testRunner.js ./test/integration ./test/integration/setup.mjs",
"test:smoke": "node test/testRunner.js ./test/smoke ./test/smoke/setup.mjs",
"test:all": "npm run test:smoke && npm run test:unit",
"test": "npm run test:smoke && npm run test:unit",
"lint": "npx eslint .",
"lint:fix": "npx eslint . --fix",
Expand Down Expand Up @@ -51,15 +51,8 @@
"eslint": "^9",
"eslint-plugin-jsdoc": "^50.2.2",
"globals": "^15.0.0",
"jest": "^29",
"typescript": ">=4.6.4"
},
"jest": {
"projects": [
"test/smoke.jest.config.js",
"test/unit.jest.config.js"
]
},
"cds": {
"typer": {
"output_directory": "@cds-models",
Expand Down
11 changes: 0 additions & 11 deletions test/int.jest.config.js

This file was deleted.

6 changes: 3 additions & 3 deletions test/integration/cds-build.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use strict'

/*
const fs = require('node:fs')
const path = require('node:path')
const { execSync } = require('node:child_process')

const dir = path.join(__dirname, '_out', 'dummy-project')
const typer = path.join(__dirname, '..', '..')

/*

beforeAll(()=>{
fs.rmSync(dir, { recursive: true, force: true })
fs.mkdirSync(dir, { recursive: true })
Expand All @@ -29,10 +29,10 @@ beforeAll(()=>{
packageJson.devDependencies['typescript'] = '*'
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
})
*/

describe('cds build', () => {
test('Dummy Project', async () => {
//execSync('cds build', { cwd: dir })
})
})
*/
17 changes: 0 additions & 17 deletions test/integration/setup.js

This file was deleted.

1 change: 1 addition & 0 deletions test/integration/setup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
7 changes: 0 additions & 7 deletions test/smoke.jest.config.js

This file was deleted.

11 changes: 0 additions & 11 deletions test/smoke/setup.js

This file was deleted.

9 changes: 9 additions & 0 deletions test/smoke/setup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as cds from '@sap/cds'
import * as util from '../util.js'

const base = util.locations.smoke.base

try {
cds.utils.fs.unlinkSync(base)
// eslint-disable-next-line no-unused-vars
} catch (_) { /* also fails on permissions, but still ignore */ }
43 changes: 30 additions & 13 deletions test/smoke/transpilation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const fs = require('fs')
const path = require('path')
const { describe, test } = require('@jest/globals')
const { describe, it } = require('node:test')
const { locations, prepareUnitTest } = require('../util')

const modelDirs = fs.readdirSync(locations.smoke.files(''))
Expand All @@ -20,19 +20,34 @@ const modelDirs = fs.readdirSync(locations.smoke.files(''))
})
.filter(Boolean)

describe('smoke', () => {
describe('transpilation', () => {
test.each(modelDirs)('$name', async ({ name, rootFile }) => {
await prepareUnitTest(
rootFile,
locations.testOutput(name),
{ transpilationCheck: true }
)
describe('transpilation', () => {
modelDirs.forEach(({ name, rootFile }) => {
it(name, async () => {
try {
await prepareUnitTest(
rootFile,
locations.testOutput(name),
{
transpilationCheck: true,
tsCompilerOptions: {
skipLibCheck: true,
}
}
)
} catch (e) {
// nodejs test runner just shows "x subtests failed" without any details
// so we are artificially showing the error here for now
// eslint-disable-next-line no-console
console.error(e)
throw e
}
})
})
})

describe('index.js CommonJS', () => {
test.each(modelDirs)('$name', async ({ name, rootFile }) => {
describe('index.js CommonJS', () => {
modelDirs.forEach(({ name, rootFile }) => {
it(name, async () => {
await prepareUnitTest(
rootFile,
locations.testOutput(name),
Expand All @@ -44,9 +59,11 @@ describe('smoke', () => {
)
})
})
})

describe('index.js ESM', () => {
test.each(modelDirs)('$name', async ({ name, rootFile }) => {
describe('index.js ESM', () => {
modelDirs.forEach(({ name, rootFile }) => {
it(name, async () => {
await prepareUnitTest(
rootFile,
locations.testOutput(name),
Expand Down
28 changes: 28 additions & 0 deletions test/testRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// TODO: this script wraps the `node --test` command until our lower bound for our test matrix is >=21 (30 Apr 2026).
// The native test runner with multiple suites requires using a glob pattern to specify the relevant test files.
// But glob is only available in Node 21 onwards. So to avoid errors where the runtime assumes it has to look for a file called "*.test.js",
// we wrap the call to node with an explicit glob expansion. Once Node22 is established as lower bound, we can have
//
// "test:unit": "node --import ./test/unit/setup.mjs --test './test/unit/*.test.js'",
// "test:integration": "node --import ./test/integration/setup.mjs --test './test/integration/*.test.js'",
// "test:smoke": "node --import ./test/smoke/setup.mjs --test './test/smoke/*.test.js'",
//
// again.
const { execFileSync } = require('child_process')
const path = require('path')
const fs = require('fs')

const testDir = process.argv[2]
const setupFile = process.argv[3]

const testFiles = fs.readdirSync(testDir)
.filter(file => file.endsWith('.test.js'))
.map(file => path.join(testDir, file))

if (testFiles.length === 0) {
// eslint-disable-next-line no-console
console.error(`No test files found in ${testDir}`)
process.exit(1)
}

execFileSync('node', ['--import', setupFile, '--test', ...testFiles], { stdio: 'inherit' })
8 changes: 0 additions & 8 deletions test/unit.jest.config.js

This file was deleted.

46 changes: 23 additions & 23 deletions test/unit/actions.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict'

const path = require('path')
const { describe, beforeAll, test, expect } = require('@jest/globals')
const { describe, before, it } = require('node:test')
const assert = require('assert')
const { checkFunction, check, ASTWrapper, checkKeyword } = require('../ast')
const { locations, prepareUnitTest } = require('../util')

Expand All @@ -11,20 +12,20 @@ describe('Actions', () => {
let astwBound
let astwUnbound

beforeAll(async () => {
before(async () => {
sut = await prepareUnitTest('actions/model.cds', locations.testOutput('actions_test'))
paths = sut.paths
astwBound = new ASTWrapper(path.join(paths.find(p => p.endsWith('S')), 'index.ts'))
astwUnbound = new ASTWrapper(path.join(paths.find(p => p.endsWith('actions_test')), 'index.ts'))
})

test('add import statement for builtin types', async () => {
expect(sut.astw.getImports()[0].module).toBe('./../_')
it('should add import statement for builtin types', async () => {
assert.strictEqual(sut.astw.getImports()[0].module, './../_')
})

test('Bound', async () => {
it('should validate bound actions', async () => {
const actions = astwBound.getAspectProperty('_EAspect', 'actions')
expect(actions.modifiers.some(check.isStatic)).toBeTruthy()
assert.ok(actions.modifiers.some(check.isStatic))
checkFunction(actions.type.members.find(fn => fn.name === 'f'), {
parameterCheck: ({members: [fst]}) => fst.name === 'x' && check.isNullable(fst.type, [check.isString])
})
Expand All @@ -40,7 +41,7 @@ describe('Actions', () => {
})
})

test('Unbound', async () => {
it('should validate unbound actions', async () => {
const ast = astwUnbound.tree
checkFunction(ast.find(node => node.name === 'free'), {
modifiersCheck: (modifiers = []) => !modifiers.some(check.isStatic),
Expand All @@ -58,9 +59,9 @@ describe('Actions', () => {
})
})

test('Bound Returning External Type', async () => {
it('should validate bound actions returning external type', async () => {
const actions = astwBound.getAspectProperty('_EAspect', 'actions')
expect(actions.modifiers.some(check.isStatic)).toBeTruthy()
assert.ok(actions.modifiers.some(check.isStatic))
checkFunction(actions.type.members.find(fn => fn.name === 'f'), {
callCheck: signature => check.isAny(signature),
parameterCheck: ({members: [fst]}) => fst.name === 'x' && check.isNullable(fst.type, [check.isString]),
Expand All @@ -78,7 +79,7 @@ describe('Actions', () => {
})
})

test('Unbound Returning External Type', async () => {
it('should validate unbound actions returning external type', async () => {
const ast = astwUnbound.tree

checkFunction(ast.find(node => node.name === 'free2'), {
Expand All @@ -94,17 +95,17 @@ describe('Actions', () => {
})
})

test('Void action returning void', async () => {
it('should validate void action returning void', async () => {
checkFunction(astwUnbound.tree.find(node => node.name === 'freevoid'), {
modifiersCheck: (modifiers = []) => !modifiers.some(check.isStatic),
callCheck: type => check.isNullable(type, [check.isVoid]),
returnTypeCheck: type => check.isNullable(type, [check.isVoid])
})
})

test('Bound Expecting $self Arguments', async () => {
it('should validate bound actions expecting $self arguments', async () => {
const actions = astwBound.getAspectProperty('_EAspect', 'actions')
expect(actions.modifiers.some(check.isStatic)).toBeTruthy()
assert.ok(actions.modifiers.some(check.isStatic))
// mainly make sure $self parameter is not present at all
checkFunction(actions.type.members.find(fn => fn.name === 's1'), {
callCheck: signature => check.isAny(signature),
Expand All @@ -127,7 +128,7 @@ describe('Actions', () => {
})
})

test ('Inflection on External Type in Function Type', async () => {
it('should validate inflection on external type in function type', async () => {
checkFunction(astwBound.tree.find(fn => fn.name === 'getOneExternalType'), {
returnTypeCheck: returns => check.isNullable(returns.subtypes[0].args[0], [st => check.isTypeReference(st, '_elsewhere.ExternalType')])
})
Expand All @@ -136,7 +137,7 @@ describe('Actions', () => {
})
})

test ('Inflection in Parameters/ Return Type of Functions', async () => {
it('should validate inflection in parameters/return type of functions', async () => {
checkFunction(astwBound.tree.find(fn => fn.name === 'fSingleParamSingleReturn'), {
parameterCheck: ({members: [fst]}) => check.isNullable(fst.type, [arg => check.isTypeReference(arg, 'E')]),
returnTypeCheck: returns => check.isNullable(returns.subtypes[0].args[0], [st => check.isTypeReference(st, 'E')])
Expand All @@ -159,7 +160,7 @@ describe('Actions', () => {
})
})

test ('Inflection in Parameters/ Return Type of Actions', async () => {
it('should validate inflection in parameters/return type of actions', async () => {
checkFunction(astwBound.tree.find(fn => fn.name === 'aSingleParamSingleReturn'), {
parameterCheck: ({members: [fst]}) => check.isNullable(fst.type, [arg => check.isTypeReference(arg, 'E')]),
returnTypeCheck: returns => check.isNullable(returns.subtypes[0].args[0], [st => check.isTypeReference(st, 'E')])
Expand All @@ -182,21 +183,20 @@ describe('Actions', () => {
})
})

test ('Optional Action Params', async () => {
it('should validate optional action params', async () => {
checkFunction(astwBound.tree.find(fn => fn.name === 'aMandatoryParam'), {
parameterCheck: ({members: [p1, p2, p3]}) => !check.isOptional(p1) && !check.isOptional(p2) && check.isOptional(p3),
})
})

test ('Empty .actions typed as empty Record', async () => {
it('should validate empty .actions typed as empty Record', async () => {
const { type } = astwUnbound.getAspectProperty('_NoActionAspect', 'actions')
expect(type.full === 'globalThis.Record'
&& checkKeyword(type.args[0], 'never')
&& checkKeyword(type.args[1], 'never')
).toBe(true)
assert.strictEqual(type.full, 'globalThis.Record')
assert.ok(checkKeyword(type.args[0], 'never'))
assert.ok(checkKeyword(type.args[1], 'never'))
})

test ('typeof Parameter Referring to Correct Type', async () => {
it('should validate typeof parameter referring to correct type', async () => {
checkFunction(astwUnbound.tree.find(node => node.name === 'freetypeof'), {
modifiersCheck: (modifiers = []) => !modifiers.some(check.isStatic),
callCheck: type => check.isNullable(type, [check.isVoid]),
Expand Down
Loading