mirror of
https://github.com/fluencelabs/aqua-vscode
synced 2025-03-14 13:20:51 +00:00
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:
parent
0ff680eb4a
commit
89fcde5d26
8
.github/workflows/run-tests.yml
vendored
8
.github/workflows/run-tests.yml
vendored
@ -22,7 +22,13 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: 'aqua-vscode'
|
||||
name: 'Integration tests'
|
||||
uses: ./.github/workflows/tests.yml
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
unit-tests:
|
||||
name: 'Unit tests'
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -17,7 +17,7 @@ env:
|
||||
|
||||
jobs:
|
||||
aqua-vscode:
|
||||
name: 'Run tests'
|
||||
name: 'Run integration tests'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
|
39
.github/workflows/unit-tests.yml
vendored
Normal file
39
.github/workflows/unit-tests.yml
vendored
Normal 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
|
@ -2,4 +2,5 @@
|
||||
.vscode-test/**
|
||||
.gitignore
|
||||
.github
|
||||
vsc-extension-quickstart.md
|
||||
integration-tests
|
||||
**/src/test
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES5",
|
||||
"target": "es2020",
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"sourceMap": true
|
||||
|
@ -128,6 +128,7 @@
|
||||
"scripts": {
|
||||
"before-tests": "bash integration-tests/before-tests.sh",
|
||||
"test": "npm run compile && vscode-test",
|
||||
"test:unit": "npm run test -C server",
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -b",
|
||||
"watch": "tsc -b -w",
|
||||
|
3
server/.mocharc.cjs
Normal file
3
server/.mocharc.cjs
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
spec: 'src/test/**/*.test.ts',
|
||||
};
|
2059
server/package-lock.json
generated
2059
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,12 @@
|
||||
"vscode-languageserver-textdocument": "^1.0.4",
|
||||
"vscode-uri": "3.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.0",
|
||||
"ts-mocha": "^10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "ts-mocha",
|
||||
"get-root": "npm root --global"
|
||||
}
|
||||
}
|
@ -48,11 +48,19 @@ export function normalizeImports(imports: unknown): Imports {
|
||||
// Deep merge two import settings, overriding the first with the second
|
||||
export function uniteImports(pre: Imports, post: Imports): Imports {
|
||||
const result: Imports = { ...pre };
|
||||
for (const [importPrefix, locations] of Object.entries(post)) {
|
||||
if (importPrefix in result) {
|
||||
result[importPrefix] = { ...result[importPrefix], ...locations };
|
||||
for (const [pathPrefix, info] of Object.entries(post)) {
|
||||
const resultInfo = result[pathPrefix];
|
||||
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 {
|
||||
result[importPrefix] = locations;
|
||||
result[pathPrefix] = info;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,8 +60,8 @@ documents.onDidClose((e) => {
|
||||
|
||||
connection.onInitialize((params: InitializeParams) => {
|
||||
connection.console.log('onInitialize event');
|
||||
const capabilities = params.capabilities;
|
||||
|
||||
const capabilities = params.capabilities;
|
||||
hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
|
||||
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
|
||||
|
||||
|
30
server/src/test/aqua/simple.aqua
Normal file
30
server/src/test/aqua/simple.aqua
Normal 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
|
109
server/src/test/imports.test.ts
Normal file
109
server/src/test/imports.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
105
server/src/test/info.test.ts
Normal file
105
server/src/test/info.test.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"allowJs": true,
|
||||
"compilerOptions": {
|
||||
"target": "ES5",
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
@ -10,6 +10,6 @@
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules", "src/test"],
|
||||
"extends": "@tsconfig/node16-strictest/tsconfig.json"
|
||||
}
|
||||
|
@ -3,13 +3,13 @@
|
||||
"declaration": true,
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES5",
|
||||
"target": "es2020",
|
||||
"outDir": "out",
|
||||
"rootDir": "src",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"types": ["node"],
|
||||
"include": [],
|
||||
"exclude": ["node_modules", ".vscode-test"],
|
||||
"references": [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user