feat(tests): Add unit tests [LNG-323] (#81)

* Add imports tests

* Add unit tests on document info

* Add workflow

* PR fixes

* Update server/src/test/info.test.ts

Co-authored-by: shamsartem <shamsartem@gmail.com>

* PR fixes

* Update target, PR fixes

* Update .vscodeignore

---------

Co-authored-by: shamsartem <shamsartem@gmail.com>
This commit is contained in:
InversionSpaces 2024-01-18 10:25:09 +01:00 committed by GitHub
parent 0ff680eb4a
commit 89fcde5d26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 2379 additions and 13 deletions

View File

@ -22,7 +22,13 @@ concurrency:
jobs: jobs:
tests: tests:
name: 'aqua-vscode' name: 'Integration tests'
uses: ./.github/workflows/tests.yml uses: ./.github/workflows/tests.yml
with: with:
ref: ${{ github.ref }} ref: ${{ github.ref }}
unit-tests:
name: 'Unit tests'
uses: ./.github/workflows/unit-tests.yml
with:
ref: ${{ github.ref }}

View File

@ -17,7 +17,7 @@ env:
jobs: jobs:
aqua-vscode: aqua-vscode:
name: 'Run tests' name: 'Run integration tests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60

39
.github/workflows/unit-tests.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Run tests with workflow_call
on:
workflow_call:
inputs:
ref:
description: 'git ref to checkout to'
type: string
default: 'main'
env:
FORCE_COLOR: true
jobs:
aqua-vscode:
name: 'Run unit tests'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: fluencelabs/aqua-vscode
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
uses: coactions/setup-xvfb@v1
with:
run: npm run test:unit

View File

@ -2,4 +2,5 @@
.vscode-test/** .vscode-test/**
.gitignore .gitignore
.github .github
vsc-extension-quickstart.md integration-tests
**/src/test

View File

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "ES5", "target": "es2020",
"outDir": "out", "outDir": "out",
"rootDir": "src", "rootDir": "src",
"sourceMap": true "sourceMap": true

View File

@ -128,6 +128,7 @@
"scripts": { "scripts": {
"before-tests": "bash integration-tests/before-tests.sh", "before-tests": "bash integration-tests/before-tests.sh",
"test": "npm run compile && vscode-test", "test": "npm run compile && vscode-test",
"test:unit": "npm run test -C server",
"vscode:prepublish": "npm run compile", "vscode:prepublish": "npm run compile",
"compile": "tsc -b", "compile": "tsc -b",
"watch": "tsc -b -w", "watch": "tsc -b -w",

3
server/.mocharc.cjs Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
spec: 'src/test/**/*.test.ts',
};

2059
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,12 @@
"vscode-languageserver-textdocument": "^1.0.4", "vscode-languageserver-textdocument": "^1.0.4",
"vscode-uri": "3.0.8" "vscode-uri": "3.0.8"
}, },
"devDependencies": {
"@types/mocha": "^10.0.0",
"ts-mocha": "^10.0.0"
},
"scripts": { "scripts": {
"test": "ts-mocha",
"get-root": "npm root --global" "get-root": "npm root --global"
} }
} }

View File

@ -48,11 +48,19 @@ export function normalizeImports(imports: unknown): Imports {
// Deep merge two import settings, overriding the first with the second // Deep merge two import settings, overriding the first with the second
export function uniteImports(pre: Imports, post: Imports): Imports { export function uniteImports(pre: Imports, post: Imports): Imports {
const result: Imports = { ...pre }; const result: Imports = { ...pre };
for (const [importPrefix, locations] of Object.entries(post)) { for (const [pathPrefix, info] of Object.entries(post)) {
if (importPrefix in result) { const resultInfo = result[pathPrefix];
result[importPrefix] = { ...result[importPrefix], ...locations }; if (resultInfo) {
for (const [importPrefix, paths] of Object.entries(info)) {
const importInfo = resultInfo[importPrefix];
if (importInfo) {
resultInfo[importPrefix] = [...importInfo, ...paths];
} else {
resultInfo[importPrefix] = paths;
}
}
} else { } else {
result[importPrefix] = locations; result[pathPrefix] = info;
} }
} }

View File

@ -60,8 +60,8 @@ documents.onDidClose((e) => {
connection.onInitialize((params: InitializeParams) => { connection.onInitialize((params: InitializeParams) => {
connection.console.log('onInitialize event'); connection.console.log('onInitialize event');
const capabilities = params.capabilities;
const capabilities = params.capabilities;
hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration); hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders); hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);

View File

@ -0,0 +1,30 @@
aqua Simple
export main
func getStr() -> string:
<- "test string"
func consumeStr(str: string) -> string:
<- str
service Srv("test-srv"):
consumeStr(str: string) -> string
ability Ab:
field: string
arrow(s: string) -> string
func main() -> string:
-- Definition
testVar <- getStr()
-- Use as argument to function
newVar1 <- consumeStr(testVar)
-- Use as argument to service
newVar2 <- Srv.consumeStr(testVar)
-- Use as argument to ability creation
ab = Ab(field = testVar, arrow = consumeStr)
-- Use as argument to ability call
newVar3 <- ab.arrow(testVar)
-- Use in return statement
<- testVar

View File

@ -0,0 +1,109 @@
import * as assert from 'assert';
import { normalizeImports, uniteImports } from '../imports';
describe('Imports Test Suite', () => {
describe('normalizeImports', () => {
it('should normalize empty imports', async () => {
assert.deepStrictEqual(normalizeImports(undefined), {});
assert.deepStrictEqual(normalizeImports(null), {});
});
it('should normalize legacy imports', async () => {
const imports = ['a', 'b', 'c'];
const normalized = {
'/': {
'': imports,
},
};
assert.deepStrictEqual(normalizeImports(imports), normalized);
});
it('should normalize imports', async () => {
const imports = {
'/': {
'': ['a', 'b', 'c'],
},
};
assert.deepStrictEqual(normalizeImports(imports), imports);
});
it('should normalize imports with single paths', async () => {
const imports = {
'/': {
'': 'a',
},
};
const normalized = {
'/': {
'': ['a'],
},
};
assert.deepStrictEqual(normalizeImports(imports), normalized);
});
it('should throw on invalid imports', async () => {
assert.throws(() => normalizeImports(123));
assert.throws(() => normalizeImports(['a', 123]));
assert.throws(() => normalizeImports({ a: 123 }));
assert.throws(() => normalizeImports({ a: { b: 123 } }));
assert.throws(() => normalizeImports({ a: { b: ['a', 123] } }));
});
});
describe('uniteImports', () => {
it('should unite distinct imports', async () => {
const lhs = {
'/left': {
'': ['l'],
},
};
const rhs = {
'/right': {
'': ['r'],
},
};
const result = { ...lhs, ...rhs };
assert.deepStrictEqual(uniteImports(lhs, rhs), result);
});
it('should unite path-intersecting imports', async () => {
const lhs = {
'/': {
left: ['l'],
},
};
const rhs = {
'/': {
right: ['r'],
},
};
const result = {
'/': {
left: ['l'],
right: ['r'],
},
};
assert.deepStrictEqual(uniteImports(lhs, rhs), result);
});
it('should unite path-prefix-intersecting imports', async () => {
const lhs = {
'/': {
'': ['l'],
},
};
const rhs = {
'/': {
'': ['r'],
},
};
const result = {
'/': {
'': ['l', 'r'],
},
};
assert.deepStrictEqual(uniteImports(lhs, rhs), result);
});
});
});

View File

@ -0,0 +1,105 @@
import * as assert from 'assert';
import * as path from 'path';
import * as fs from 'fs/promises';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { Location, Position, Range } from 'vscode-languageserver';
import { compileAqua } from '../validation';
import { DocumentInfo } from '../info';
import { pathToUri, tokenToLocation } from '../utils';
/**
* Load document from file, compile it and return info
* @param relPath path relative to test folder
* @returns document, document info after compilation
*/
async function openDocument(relPath: string): Promise<[TextDocument, DocumentInfo]> {
const absPath = path.join(__dirname, relPath);
const content = await fs.readFile(absPath, 'utf-8');
const document = TextDocument.create(pathToUri(absPath), 'aqua', 0, content);
// Compile without imports
const [_, info] = await compileAqua({ imports: {} }, document);
return [document, info];
}
/**
* Find all locations of a variable in a document
* @param name variable name
* @param doc text document to search in
* @returns locations of variable in document
*/
function locationsOf(name: string, doc: TextDocument): Location[] {
const regex = new RegExp(`(?<=\\b)${name}(?=\\b)`, 'g');
return [...doc.getText().matchAll(regex)].map((match) => {
// `index` will always be presented, see
// https://github.com/microsoft/TypeScript/issues/36788
const index = match.index as number;
return {
uri: doc.uri,
range: {
start: doc.positionAt(index),
end: doc.positionAt(index + match[0].length),
},
};
});
}
/**
* Generate all positions in a range
* @param doc text document
* @param range range
* @returns positions in range
*/
function genRange(doc: TextDocument, range: Range): Position[] {
const positions: Position[] = [];
const begOff = doc.offsetAt(range.start);
const endOff = doc.offsetAt(range.end);
for (let off = begOff; off < endOff; off++) {
positions.push(doc.positionAt(off));
}
return positions;
}
describe('DocumentInfo Test Suite', () => {
describe('infoAt', () => {
it('should return type information on each occurrence (simple)', async () => {
const [document, docInfo] = await openDocument('aqua/simple.aqua');
const locations = locationsOf('testVar', document);
assert.strictEqual(locations.length, 6, 'Not all occurrences found');
for (const loc of locations) {
for (const pos of genRange(document, loc.range)) {
const info = docInfo.infoAt(pos);
assert.ok(info, 'Info not found');
assert.strictEqual(info.type, 'string', 'Wrong type info');
}
}
});
});
describe('defAt', () => {
it('should return definition location on each occurrence (simple)', async () => {
const [document, docInfo] = await openDocument('aqua/simple.aqua');
const locations = locationsOf('testVar', document);
assert.strictEqual(locations.length, 6, 'Not all occurrences found');
const definition = locations[0];
for (const loc of locations.slice(1)) {
for (const pos of genRange(document, loc.range)) {
const def = docInfo.defAt(pos);
assert.ok(def, 'Definition not found');
assert.deepStrictEqual(tokenToLocation(def), definition, 'Wrong definition location');
}
}
});
});
});

View File

@ -1,7 +1,7 @@
{ {
"allowJs": true, "allowJs": true,
"compilerOptions": { "compilerOptions": {
"target": "ES5", "target": "es2020",
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"sourceMap": true, "sourceMap": true,
@ -10,6 +10,6 @@
"rootDir": "src" "rootDir": "src"
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules"], "exclude": ["node_modules", "src/test"],
"extends": "@tsconfig/node16-strictest/tsconfig.json" "extends": "@tsconfig/node16-strictest/tsconfig.json"
} }

View File

@ -3,13 +3,13 @@
"declaration": true, "declaration": true,
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "ES5", "target": "es2020",
"outDir": "out", "outDir": "out",
"rootDir": "src", "rootDir": "src",
"sourceMap": true "sourceMap": true
}, },
"include": ["src"],
"types": ["node"], "types": ["node"],
"include": [],
"exclude": ["node_modules", ".vscode-test"], "exclude": ["node_modules", ".vscode-test"],
"references": [ "references": [
{ {