├── .gitignore ├── .npmignore ├── License.txt ├── README.md ├── bin └── server.js ├── package-lock.json ├── package.json ├── screenshots ├── codelens.png └── diagnostic.gif ├── src ├── filesystem │ ├── adapter.ts │ ├── common.ts │ ├── contract.ts │ ├── index.ts │ ├── posix.ts │ └── windows.ts ├── helpers.ts ├── phpunit │ ├── ast.ts │ ├── collection.ts │ ├── common.ts │ ├── index.ts │ ├── junit.ts │ ├── parameters.ts │ ├── phpunit.ts │ └── textline-range.ts ├── process.ts ├── providers │ ├── codelens-provider.ts │ ├── document-symbol-provider.ts │ └── index.ts ├── runner.ts └── server.ts ├── tests ├── filesystem │ ├── adapter.test.ts │ ├── posix.test.ts │ └── windows.test.ts ├── fixtures │ ├── PHPUnitTest.php │ ├── bin │ │ ├── cmd │ │ ├── cmd.exe │ │ └── ls │ ├── junit.xml │ ├── project │ │ ├── .gitignore │ │ ├── .php_cs │ │ ├── composer.json │ │ ├── junit.xml │ │ ├── phpunit.xml.dist │ │ ├── src │ │ │ ├── Calculator.php │ │ │ └── Item.php │ │ ├── teamcity.txt │ │ ├── tests │ │ │ ├── AssertionsTest.php │ │ │ ├── CalculatorTest.php │ │ │ └── bootstrap.php │ │ └── vendor-stub │ │ │ ├── bin │ │ │ ├── phpunit │ │ │ └── phpunit.bat │ │ │ └── mockery │ │ │ └── mockery │ │ │ └── library │ │ │ ├── Mockery.php │ │ │ └── Mockery │ │ │ ├── Container.php │ │ │ ├── CountValidator │ │ │ └── Exact.php │ │ │ ├── Exception.php │ │ │ ├── Expectation.php │ │ │ └── ExpectationDirector.php │ ├── teamcity.txt │ └── usr │ │ └── bin │ │ └── php ├── helpers.ts ├── phpunit │ ├── ast.test.ts │ ├── collection.test.ts │ ├── junit.test.ts │ ├── parameters.test.ts │ └── phpunit.test.ts ├── process.test.ts └── providers │ ├── codelens-provider.test.ts │ └── document-symbol-provider.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | master -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tests 3 | src 4 | screenshots -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation 2 | 3 | All rights reserved. 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 8 | (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 16 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 17 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpunit-language-server 2 | 3 | phpunit-language-server is a server implementation that provides PHPUnit smartness. 4 | The server adheres to the [language server protocol](https://github.com/Microsoft/language-server-protocol) 5 | and can be used with any editor that supports the protocol. The server utilizes [PHPUnit](https://phpunit.de). 6 | 7 | ## Clients 8 | -------------- 9 | 10 | These clients are available: 11 | * [VS Code](https://marketplace.visualstudio.com/items?temName=recca0120.vscode-phpunit) 12 | 13 | ## Features 14 | -------------- 15 | 16 | In the current implementation we support following language features. 17 | 18 | - [x] Code lens (references) 19 | 20 | ![CodeLens](screenshots/codelens.png) 21 | 22 | - [x] Publish Diagnostics 23 | 24 | ![Publish Diagnostics](screenshots/diagnostic.gif) 25 | 26 | - [ ] Code completion 27 | 28 | ## Features planned 29 | -------------- 30 | 31 | - As you type reporting of parsing and compilation errors 32 | 33 | ## Installation 34 | 35 | ```bash 36 | npm i -g phpunit-language-server 37 | ``` 38 | 39 | ## Execute 40 | 41 | ```bash 42 | phpunit-language-server 43 | ``` 44 | 45 | Feedback 46 | --------- 47 | * File a bug in [GitHub Issues](https://github.com/recca0120/phpunit-language-server/issues). 48 | 49 | License 50 | ------- 51 | MIT, See [LICENSE](LICENSE.txt) file. -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | process.title = 'phpunit language server'; 4 | 5 | if (process.argv.some(arg => ['--stdio', '--node-ipc'].indexOf(arg) !== -1) === false) { 6 | process.argv.push('--stdio'); 7 | } 8 | 9 | require('../dist/server'); 10 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpunit-language-server", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/he": { 8 | "version": "0.5.29", 9 | "resolved": "https://registry.npmjs.org/@types/he/-/he-0.5.29.tgz", 10 | "integrity": "sha1-Z/k5IoewVVgBPRyryWgBpRha2+o=" 11 | }, 12 | "doc-parser": { 13 | "version": "0.4.8", 14 | "resolved": "https://registry.npmjs.org/doc-parser/-/doc-parser-0.4.8.tgz", 15 | "integrity": "sha1-sCNRV+3UfhM/QAy3cFXj4WuNFnk=" 16 | }, 17 | "fast-xml-parser": { 18 | "version": "3.3.7", 19 | "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.3.7.tgz", 20 | "integrity": 21 | "sha512-JEATv+TKhTkcyGazSX2dFwQrHn/PCcTbYXLQk3d6+9wBqDYG2cuBcnXzLLD2dzaFTEPwbE+kaF25ngrGSudj2A==", 22 | "requires": { 23 | "nimnjs": "1.2.2" 24 | } 25 | }, 26 | "he": { 27 | "version": "1.1.1", 28 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 29 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" 30 | }, 31 | "nimn-date-parser": { 32 | "version": "1.0.0", 33 | "resolved": "https://registry.npmjs.org/nimn-date-parser/-/nimn-date-parser-1.0.0.tgz", 34 | "integrity": 35 | "sha512-1Nf+x3EeMvHUiHsVuEhiZnwA8RMeOBVTQWfB1S2n9+i6PYCofHd2HRMD+WOHIHYshy4T4Gk8wQoCol7Hq3av8Q==" 36 | }, 37 | "nimn_schema_builder": { 38 | "version": "1.1.0", 39 | "resolved": "https://registry.npmjs.org/nimn_schema_builder/-/nimn_schema_builder-1.1.0.tgz", 40 | "integrity": 41 | "sha512-DK5/B8CM4qwzG2URy130avcwPev4uO0ev836FbQyKo1ms6I9z/i6EJyiZ+d9xtgloxUri0W+5gfR8YbPq7SheA==" 42 | }, 43 | "nimnjs": { 44 | "version": "1.2.2", 45 | "resolved": "https://registry.npmjs.org/nimnjs/-/nimnjs-1.2.2.tgz", 46 | "integrity": 47 | "sha512-gpaKuOjoAui+8EpxRJXeZDBYJGVr9mlG7RC8TDzDxLGLr+GdH/6tACVZjbVtA7b1iJBkX6sZSmS56p4TDIDgxQ==", 48 | "requires": { 49 | "nimn-date-parser": "1.0.0", 50 | "nimn_schema_builder": "1.1.0" 51 | } 52 | }, 53 | "php-parser": { 54 | "version": "3.0.0-alpha2", 55 | "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.0.0-alpha2.tgz", 56 | "integrity": "sha1-bcORysgJ5UFzjxxz9uy52ECjiEA=" 57 | }, 58 | "vscode-jsonrpc": { 59 | "version": "3.6.1", 60 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.1.tgz", 61 | "integrity": 62 | "sha512-+Eb+Dxf2kC2h079msx61hkblxAKE0S2j78+8QpnigLAO2aIIjkCwTIH34etBrU8E8VItRinec7YEwULx9at5bQ==" 63 | }, 64 | "vscode-languageserver": { 65 | "version": "4.1.2", 66 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-4.1.2.tgz", 67 | "integrity": 68 | "sha512-3iej2tuMaI9yirPXF7/fVyIvBhSzbwZ3EWFRb8bP6lc3tGv9SJHDaJLNyQMgo9J8CNpXil6dWarpJvGSA60v/w==", 69 | "requires": { 70 | "vscode-languageserver-protocol": "3.7.1", 71 | "vscode-uri": "1.0.3" 72 | } 73 | }, 74 | "vscode-languageserver-protocol": { 75 | "version": "3.7.1", 76 | "resolved": 77 | "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.7.1.tgz", 78 | "integrity": 79 | "sha512-AKX9XQ49m/lpiDLZJBypFNc5eAXNlSecunYU5m4o5WIwGgW86TWnXVdziuFm47W2SdigDa/jVbxLPSNUeut9fQ==", 80 | "requires": { 81 | "vscode-jsonrpc": "3.6.1", 82 | "vscode-languageserver-types": "3.7.1" 83 | } 84 | }, 85 | "vscode-languageserver-types": { 86 | "version": "3.7.1", 87 | "resolved": 88 | "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.7.1.tgz", 89 | "integrity": 90 | "sha512-ftGfU79AnnI3OHCG7kzCCN47jNI7BjECPAH2yhddtYTiQk0bnFbuFeQKvpXQcyNI3GsKEx5b6kSiBYshTiep6w==" 91 | }, 92 | "vscode-uri": { 93 | "version": "1.0.3", 94 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.3.tgz", 95 | "integrity": "sha1-Yxvb9xbcyrDmUpGo3CXCMjIIWlI=" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpunit-language-server", 3 | "description": "phpunit language server", 4 | "version": "0.0.13", 5 | "author": "recca0120", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/recca0120/phpunit-language-server" 13 | }, 14 | "dependencies": { 15 | "@types/he": "^0.5.29", 16 | "doc-parser": "^0.4.8", 17 | "fast-xml-parser": "^3.3.8", 18 | "he": "^1.1.1", 19 | "php-parser": "^3.0.0-alpha2", 20 | "vscode-languageserver": "^4.1.2", 21 | "vscode-uri": "^1.0.3" 22 | }, 23 | "scripts": { 24 | "installServer": "installServerIntoExtension ../client ./package.json ./tsconfig.json", 25 | "compile": "installServerIntoExtension ../client ./package.json ./tsconfig.json && tsc -p .", 26 | "watch": "installServerIntoExtension ../client ./package.json ./tsconfig.json && tsc -w -p .", 27 | "prepublishOnly": "tsc -p ./ --outDir dist/" 28 | }, 29 | "main": "./dist/server.js", 30 | "types": "./dist/server.d.ts", 31 | "bin": { 32 | "phpunit-language-server": "./bin/server.js" 33 | }, 34 | "keywords": ["language server protocol", "phpunit", "php", "test", "unittest"] 35 | } 36 | -------------------------------------------------------------------------------- /screenshots/codelens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recca0120/phpunit-language-server/1c4b5e0d8801613fa363760fb61c2e2516a2e2d3/screenshots/codelens.png -------------------------------------------------------------------------------- /screenshots/diagnostic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recca0120/phpunit-language-server/1c4b5e0d8801613fa363760fb61c2e2516a2e2d3/screenshots/diagnostic.gif -------------------------------------------------------------------------------- /src/filesystem/adapter.ts: -------------------------------------------------------------------------------- 1 | import { FilesystemContract } from './contract'; 2 | import { OS, os } from '../helpers'; 3 | import { POSIX } from './posix'; 4 | import { WINDOWS } from './windows'; 5 | 6 | export class Adapter implements FilesystemContract { 7 | constructor(private instance: FilesystemContract = os() === OS.WIN ? new WINDOWS() : new POSIX()) {} 8 | 9 | exists(path: string): Promise { 10 | return this.instance.exists(path); 11 | } 12 | 13 | get(path: string): Promise { 14 | return this.instance.get(path); 15 | } 16 | 17 | where(search: string, cwd: string = process.cwd()): Promise { 18 | return this.instance.where(search, cwd); 19 | } 20 | 21 | which(search: string, cwd: string = process.cwd()): Promise { 22 | return this.instance.which(search, cwd); 23 | } 24 | 25 | findUp(search: string, cwd: string = process.cwd(), root?: string): Promise { 26 | return this.instance.findUp(search, cwd, root); 27 | } 28 | 29 | normalizePath(path: string): string { 30 | return this.instance.normalizePath(path); 31 | } 32 | 33 | setSystemPaths(systemPaths: string): FilesystemContract { 34 | this.instance.setSystemPaths(systemPaths); 35 | 36 | return this; 37 | } 38 | 39 | getSystemPaths(): string[] { 40 | throw this.instance.getSystemPaths(); 41 | } 42 | 43 | dirname(path: string): string { 44 | return this.instance.dirname(path); 45 | } 46 | 47 | tmpfile(extension: string = 'tmp', prefix?: string): string { 48 | return this.instance.tmpfile(extension, prefix); 49 | } 50 | 51 | unlink(path: string): Promise { 52 | return this.instance.unlink(path); 53 | } 54 | 55 | uri(path: string): string { 56 | return this.instance.uri(path); 57 | } 58 | } 59 | 60 | export class Filesystem extends Adapter {} 61 | -------------------------------------------------------------------------------- /src/filesystem/common.ts: -------------------------------------------------------------------------------- 1 | import { readFile, stat, unlink } from 'fs'; 2 | import { FilesystemContract } from './contract'; 3 | import { resolve as pathResolve, parse, dirname } from 'path'; 4 | import { tmpdir } from 'os'; 5 | import { default as Uri } from 'vscode-uri'; 6 | 7 | export abstract class Common implements FilesystemContract { 8 | protected systemPaths: string[] = []; 9 | protected delimiter: string = '/'; 10 | protected extensions: string[] = []; 11 | 12 | exists(path: string): Promise { 13 | return new Promise(resolve => { 14 | stat(this.normalizePath(path), err => { 15 | resolve(err && err.code === 'ENOENT' ? false : true); 16 | }); 17 | }); 18 | } 19 | 20 | get(path: string): Promise { 21 | return new Promise((resolve, reject) => { 22 | return readFile(this.normalizePath(path), (err, buffer) => { 23 | err ? reject(err) : resolve(buffer.toString('utf8')); 24 | }); 25 | }); 26 | } 27 | 28 | getSystemPaths(): string[] { 29 | return this.systemPaths; 30 | } 31 | 32 | async where(search: string, cwd: string = process.cwd()): Promise { 33 | const paths: string[] = [cwd].concat(this.getSystemPaths()); 34 | const extensions = this.extensions; 35 | for (const path of paths) { 36 | for (const ext of extensions) { 37 | const file = pathResolve(path, `${search}${ext}`); 38 | if ((await this.exists(file)) === true) { 39 | return file; 40 | } 41 | } 42 | } 43 | 44 | return ''; 45 | } 46 | 47 | async which(search: string, cwd: string = process.cwd()): Promise { 48 | return this.where(search, cwd); 49 | } 50 | 51 | async findUp(search: string, cwd: string = process.cwd(), root?: string): Promise { 52 | let file: string; 53 | root = pathResolve(!root ? parse(cwd).root : root); 54 | cwd = pathResolve(cwd); 55 | 56 | do { 57 | file = pathResolve(cwd, search); 58 | if ((await this.exists(file)) === true) { 59 | return file; 60 | } 61 | 62 | if (cwd === root) { 63 | break; 64 | } 65 | 66 | cwd = pathResolve(cwd, '..'); 67 | } while (cwd !== root); 68 | 69 | file = pathResolve(cwd, search); 70 | 71 | return (await this.exists(file)) === true ? file : ''; 72 | } 73 | 74 | setSystemPaths(systemPaths: string): FilesystemContract { 75 | this.systemPaths = systemPaths 76 | .split(new RegExp(this.delimiter, 'g')) 77 | .map((path: string) => path.replace(new RegExp(`${this.delimiter}$`, 'g'), '').trim()); 78 | 79 | return this; 80 | } 81 | 82 | dirname(path: string): string { 83 | return dirname(path); 84 | } 85 | 86 | tmpfile(extension: string = 'tmp', prefix?: string): string { 87 | prefix = prefix ? `${prefix}-` : ''; 88 | 89 | return pathResolve(tmpdir(), `${prefix}${new Date().getTime()}.${extension}`); 90 | } 91 | 92 | unlink(path: string): Promise { 93 | return new Promise(resolve => 94 | unlink(path, (error: NodeJS.ErrnoException | undefined) => resolve(error ? false : true)) 95 | ); 96 | } 97 | 98 | uri(path: string): string { 99 | return this.isUri(path) === true ? Uri.parse(path).toString() : Uri.file(path).toString(); 100 | } 101 | 102 | private isUri(uri: string): boolean { 103 | return /^file:\/\//.test(uri); 104 | } 105 | 106 | abstract normalizePath(path: string): string; 107 | } 108 | -------------------------------------------------------------------------------- /src/filesystem/contract.ts: -------------------------------------------------------------------------------- 1 | export interface FilesystemContract { 2 | exists(path: string): Promise; 3 | get(path: string): Promise; 4 | normalizePath(path: string): string; 5 | setSystemPaths(systemPaths: string): FilesystemContract; 6 | getSystemPaths(): string[]; 7 | where(search: string, cwd?: string): Promise; 8 | which(search: string, cwd?: string): Promise; 9 | findUp(search: string, cwd?: string, root?: string): Promise; 10 | dirname(path: string): string; 11 | tmpfile(extension?: string, prefix?: string): string; 12 | unlink(path: string): Promise; 13 | uri(path: string): string; 14 | } 15 | -------------------------------------------------------------------------------- /src/filesystem/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contract'; 2 | export * from './common'; 3 | export * from './posix'; 4 | export * from './windows'; 5 | export * from './adapter'; 6 | -------------------------------------------------------------------------------- /src/filesystem/posix.ts: -------------------------------------------------------------------------------- 1 | import { Common } from './common'; 2 | 3 | export class POSIX extends Common { 4 | protected separator: string = '/'; 5 | protected delimiter: string = ':'; 6 | protected extensions: string[] = ['']; 7 | 8 | constructor() { 9 | super(); 10 | this.setSystemPaths(process.env.PATH as string); 11 | } 12 | 13 | normalizePath(path: string): string { 14 | return path.replace(/^file:\/\//, '').replace(/ /g, '\\ '); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/filesystem/windows.ts: -------------------------------------------------------------------------------- 1 | import { POSIX } from './posix'; 2 | 3 | export class WINDOWS extends POSIX { 4 | protected separator: string = '\\'; 5 | protected delimiter: string = ';'; 6 | protected extensions = ['.bat', '.exe', '.cmd', '']; 7 | 8 | constructor() { 9 | super(); 10 | this.setSystemPaths(process.env.PATH as string); 11 | } 12 | 13 | normalizePath(path: string): string { 14 | return ( 15 | path 16 | .replace(/^file:\/\//, '') 17 | .replace(/^\/(\w)(%3A|:)/, '$1:') 18 | // .replace(/^\w:/, m => m.toUpperCase()) 19 | .replace(/\//g, this.separator) 20 | .replace(/ /g, '\\ ') 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export enum OS { 2 | WINDOWS = 1, 3 | WIN = 1, 4 | POSIX = 2, 5 | LINUX = 2, 6 | } 7 | 8 | export function os(): OS { 9 | return /win32|mswin(?!ce)|mingw|bccwin|cygwin/i.test(process.platform) ? OS.WIN : OS.POSIX; 10 | } 11 | 12 | export function tap(value: T, callback: Function): T { 13 | callback(value); 14 | 15 | return value; 16 | } 17 | 18 | export function value(value: T, callback: Function): T { 19 | return callback(value); 20 | } 21 | 22 | export function when(value: T, success: any, fail?: any): any { 23 | if (value) { 24 | return success instanceof Function ? success(value) : success; 25 | } else if (fail) { 26 | return fail instanceof Function ? fail(value) : fail; 27 | } 28 | 29 | return ''; 30 | } 31 | 32 | export function groupBy(items: any[], key: string): Map { 33 | return items.reduce((groups: Map, item: any) => { 34 | const group: any[] = groups.get(item[key]) || []; 35 | group.push(item); 36 | groups.set(item[key], group); 37 | 38 | return groups; 39 | }, new Map()); 40 | } 41 | -------------------------------------------------------------------------------- /src/phpunit/ast.ts: -------------------------------------------------------------------------------- 1 | import { default as Engine, Program } from 'php-parser'; 2 | import { Range } from 'vscode-languageserver-types'; 3 | import { TestNode } from './common'; 4 | 5 | export class Ast { 6 | parse(code: string, uri: string): TestNode[] { 7 | return this.getTestNodes( 8 | Engine.parseCode(code, { 9 | ast: { 10 | withPositions: true, 11 | withSource: true, 12 | }, 13 | parser: { 14 | debug: false, 15 | extractDoc: true, 16 | suppressErrors: true, 17 | }, 18 | lexer: { 19 | all_tokens: true, 20 | comment_tokens: true, 21 | mode_eval: true, 22 | asp_tags: true, 23 | short_tags: true, 24 | }, 25 | }), 26 | uri 27 | ); 28 | } 29 | 30 | getTestNodes(node: Program, uri: string): TestNode[] { 31 | return node.children.reduce((classes: any[], nsOrClass: any) => { 32 | const namespace: string = nsOrClass.kind === 'namespace' ? nsOrClass.name : ''; 33 | 34 | return (nsOrClass.kind === 'namespace' ? classes.concat(nsOrClass.children) : classes.concat(nsOrClass)) 35 | .filter((o: any) => this.isClass(o)) 36 | .reduce((c: TestNode[], o: any) => c.concat(this.convertToTestNodes(o, uri, namespace)), []); 37 | }, []); 38 | } 39 | 40 | private convertToTestNodes(node: any, uri: string, namespace: string): TestNode[] { 41 | const oClass: string = namespace ? `${namespace}\\${node.name}` : node.name; 42 | const classname: string = oClass.replace(/\\/g, '.'); 43 | 44 | const methods: TestNode[] = node.body 45 | .filter((method: any) => this.isTestMethod(method)) 46 | .map((method: any) => this.convertToTestNode(method, uri, oClass, classname)); 47 | 48 | return methods.length === 0 ? [] : [this.convertToTestNode(node, uri, oClass, classname)].concat(methods); 49 | } 50 | 51 | private convertToTestNode(node: any, uri: string, oClass: string, classname: string) { 52 | const { start } = node.loc; 53 | 54 | return { 55 | class: oClass, 56 | classname: classname, 57 | name: node.kind === 'method' ? node.name : oClass, 58 | uri: uri, 59 | range: Range.create(start.line - 1, start.column, start.line - 1, start.column + node.name.length), 60 | }; 61 | } 62 | 63 | private isClass(node: any): boolean { 64 | return node.kind === 'class' && node.isAbstract === false; 65 | } 66 | 67 | private isTestMethod(node: any): boolean { 68 | return ( 69 | node.isAbstract === false && 70 | node.kind === 'method' && 71 | // /markTest(Skipped|Incomplete)/.test(node.body.loc.source) === false && 72 | (/^test/.test(node.name) === true || 73 | (node.leadingComments && 74 | node.leadingComments.some((comment: any) => /@test/.test(comment.value)) === true)) 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/phpunit/collection.ts: -------------------------------------------------------------------------------- 1 | import { Assertion, Detail, Test, TestNode, Type } from './common'; 2 | import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity } from 'vscode-languageserver-types'; 3 | import { Filesystem, FilesystemContract } from '../filesystem'; 4 | import { groupBy, tap } from '../helpers'; 5 | 6 | export class Collection { 7 | private errorTypes: Type[] = [Type.ERROR, Type.FAILED, Type.FAILURE, Type.RISKY]; 8 | private items: Map = new Map(); 9 | 10 | constructor(private files: FilesystemContract = new Filesystem()) {} 11 | 12 | put(tests: Test[]): Collection { 13 | const groups: Map = groupBy(tests, 'uri'); 14 | 15 | for (const key of groups.keys()) { 16 | this.items.set(key, this.merge(this.items.get(key) || [], groups.get(key) || [])); 17 | } 18 | 19 | return this; 20 | } 21 | 22 | get(uri: string): Test[] { 23 | return this.items.get(this.files.uri(uri)) || []; 24 | } 25 | 26 | map(callback: Function): any[] { 27 | const items: any[] = []; 28 | 29 | this.forEach((tests: Test[], uri: string) => { 30 | items.push(callback(tests, uri)); 31 | }); 32 | 33 | return items; 34 | } 35 | 36 | forEach(callback: Function): Collection { 37 | this.items.forEach((tests: Test[], uri: string) => { 38 | callback(tests, uri); 39 | }); 40 | 41 | return this; 42 | } 43 | 44 | all(): Map { 45 | return this.items; 46 | } 47 | 48 | getTestNodes(uri: string): TestNode[] { 49 | return (this.getAssertions().get(uri) || []).map((assertion: Assertion) => { 50 | return Object.assign({}, this.cloneTest(assertion.related), { 51 | range: assertion.range, 52 | }); 53 | }); 54 | } 55 | 56 | getDiagnoics(): Map { 57 | return tap(new Map(), (map: Map) => { 58 | this.forEach((tests: Test[], uri: string) => { 59 | map.set( 60 | uri, 61 | tests.filter(this.filterByType.bind(this)).map((test: Test) => this.transformToDiagonstic(test)) 62 | ); 63 | }); 64 | }); 65 | } 66 | 67 | getAssertions(keepDetails: boolean = false): Map { 68 | const assertions: Assertion[] = []; 69 | this.forEach((tests: Test[]) => { 70 | tests.forEach((test: Test) => { 71 | const details: Detail[] = (test.fault && test.fault.details) || []; 72 | const related: Test = this.cloneTest(test, keepDetails); 73 | 74 | assertions.push({ 75 | uri: test.uri, 76 | range: test.range, 77 | related: related, 78 | }); 79 | 80 | details.forEach((detail: Detail) => { 81 | assertions.push({ 82 | uri: detail.uri, 83 | range: detail.range, 84 | related: related, 85 | }); 86 | }); 87 | }); 88 | }); 89 | 90 | return groupBy(assertions, 'uri'); 91 | } 92 | 93 | private transformToDiagonstic(test: Test): Diagnostic { 94 | return { 95 | severity: test.type === Type.RISKY ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error, 96 | range: test.range, 97 | message: test.fault ? test.fault.message : '', 98 | relatedInformation: this.transformToRelatedInformation(test), 99 | source: 'PHPUnit', 100 | }; 101 | } 102 | 103 | private transformToRelatedInformation(test: Test): DiagnosticRelatedInformation[] { 104 | if (!test.fault || !test.fault.details) { 105 | return []; 106 | } 107 | const message: string = test.fault.message; 108 | const details = test.fault && test.fault.details ? test.fault.details : []; 109 | 110 | return details.map((detail: Detail) => { 111 | return { 112 | location: detail, 113 | message: message, 114 | }; 115 | }); 116 | } 117 | 118 | private filterByType(test: Test): boolean { 119 | return this.errorTypes.indexOf(test.type) !== -1; 120 | } 121 | 122 | private merge(oldTests: Test[], newTests: Test[]): Test[] { 123 | const merged: Test[] = oldTests 124 | .filter((oldTest: Test) => { 125 | for (const newTest of newTests) { 126 | if (oldTest.uri === newTest.uri && oldTest.name === newTest.name) { 127 | return false; 128 | } 129 | } 130 | 131 | return true; 132 | }) 133 | .concat(newTests); 134 | 135 | merged.sort((a: Test, b: Test) => { 136 | return a.range.start.line > b.range.start.line ? 1 : -1; 137 | }); 138 | 139 | return merged; 140 | } 141 | 142 | private cloneTest(test: Test, keepDetails: boolean = false): Test { 143 | return tap( 144 | { 145 | name: test.name, 146 | class: test.class, 147 | classname: test.classname, 148 | uri: test.uri, 149 | range: test.range, 150 | time: test.time, 151 | type: test.type, 152 | }, 153 | (related: Test) => { 154 | if (!test.fault) { 155 | return; 156 | } 157 | 158 | related.fault = { 159 | type: test.fault.type, 160 | message: test.fault.message, 161 | }; 162 | 163 | if (keepDetails === true) { 164 | related.fault.details = test.fault.details; 165 | } 166 | } 167 | ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/phpunit/common.ts: -------------------------------------------------------------------------------- 1 | import { Location } from 'vscode-languageserver-types'; 2 | 3 | export interface FaultNode { 4 | type: Type; 5 | _type: string; 6 | __text: string; 7 | } 8 | 9 | export interface Node { 10 | _name: string; 11 | _class: string; 12 | _classname: string; 13 | _file: string; 14 | _line: string; 15 | _assertions: string; 16 | _time: string; 17 | error?: FaultNode; 18 | warning?: FaultNode; 19 | failure?: FaultNode; 20 | skipped?: string; 21 | incomplete?: string; 22 | } 23 | 24 | export enum Type { 25 | PASSED = 'passed', 26 | ERROR = 'error', 27 | WARNING = 'warning', 28 | FAILURE = 'failure', 29 | INCOMPLETE = 'incomplete', 30 | RISKY = 'risky', 31 | SKIPPED = 'skipped', 32 | FAILED = 'failed', 33 | } 34 | 35 | export interface Detail extends Location {} 36 | 37 | export interface Fault { 38 | message: string; 39 | type: string; 40 | details?: Detail[]; 41 | } 42 | 43 | export interface TestNode extends Location { 44 | name: string; 45 | class: string; 46 | classname: string; 47 | } 48 | 49 | export interface Test extends TestNode { 50 | time: number; 51 | type: Type; 52 | fault?: Fault; 53 | } 54 | 55 | export interface Assertion extends Detail { 56 | related: Test; 57 | } 58 | -------------------------------------------------------------------------------- /src/phpunit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ast'; 2 | export * from './collection'; 3 | export * from './common'; 4 | export * from './junit'; 5 | export * from './parameters'; 6 | export * from './phpunit'; 7 | export * from './textline-range'; 8 | -------------------------------------------------------------------------------- /src/phpunit/junit.ts: -------------------------------------------------------------------------------- 1 | import { decode } from 'he'; 2 | import { Detail, FaultNode, Node, Test, Type } from './common'; 3 | import { Filesystem, FilesystemContract } from '../filesystem'; 4 | import { tap, value, when } from '../helpers'; 5 | import { TextlineRange } from './textline-range'; 6 | 7 | const parse = require('fast-xml-parser').parse; 8 | 9 | export class JUnit { 10 | private pathPattern: RegExp = /(.*):(\d+)$/; 11 | 12 | constructor( 13 | private files: FilesystemContract = new Filesystem(), 14 | private textLineRange: TextlineRange = new TextlineRange() 15 | ) {} 16 | 17 | async parseFile(path: string): Promise { 18 | return path && (await this.files.exists(path)) 19 | ? tap(await this.parse(await this.files.get(path)), () => { 20 | this.files.unlink(path); 21 | }) 22 | : []; 23 | } 24 | 25 | async parse(code: string): Promise { 26 | return tap( 27 | await this.getTests( 28 | this.getNodes( 29 | parse(code, { 30 | attributeNamePrefix: '_', 31 | ignoreAttributes: false, 32 | ignoreNameSpace: false, 33 | parseNodeValue: true, 34 | parseAttributeValue: true, 35 | trimValues: true, 36 | textNodeName: '__text', 37 | }) 38 | ) 39 | ), 40 | () => this.textLineRange.clear() 41 | ); 42 | } 43 | 44 | private getSuites(node: any): any[] { 45 | const suite: any = this.getSuite(node); 46 | 47 | return suite instanceof Array 48 | ? suite.reduce((suites: any[], suite: any) => { 49 | return suites.concat(this.getSuites(suite)); 50 | }, []) 51 | : [suite]; 52 | } 53 | 54 | private getSuite(node: any): any { 55 | return when( 56 | node.testsuite, 57 | (testsuite: any) => { 58 | while (testsuite.testsuite) { 59 | testsuite = testsuite.testsuite; 60 | } 61 | 62 | return testsuite; 63 | }, 64 | node 65 | ); 66 | } 67 | 68 | private getNodes(node: any): Node[] { 69 | return this.getSuites(node.testsuites).reduce((tests: any[], suite: any) => { 70 | return tests.concat(suite.testcase); 71 | }, []); 72 | } 73 | 74 | private getTests(nodes: Node[]): Promise { 75 | return Promise.all(nodes.map(this.parseTest.bind(this)) as Promise[]); 76 | } 77 | 78 | private async parseTest(node: Node): Promise { 79 | return this.parseFault( 80 | Object.assign(await this.createLocation(node._file, parseInt(node._line, 10) || 1), { 81 | name: node._name || '', 82 | class: node._class, 83 | classname: node._classname || '', 84 | time: parseFloat(node._time) || 0, 85 | type: Type.PASSED, 86 | }), 87 | node 88 | ); 89 | } 90 | 91 | private parseFault(test: Test, node: Node): Promise { 92 | return when( 93 | this.getFaultNode(node), 94 | async (fault: FaultNode) => { 95 | const details: Detail[] = await this.parseDetails(fault); 96 | const current: Detail = this.current(details, test); 97 | const message: string = this.parseMessage(fault); 98 | 99 | return Object.assign(test, current, { 100 | type: fault.type, 101 | fault: { 102 | type: fault._type || '', 103 | message: message, 104 | details: this.filterDetails(details, current), 105 | }, 106 | }); 107 | }, 108 | test 109 | ); 110 | } 111 | 112 | private getFaultNode(node: Node): FaultNode | undefined { 113 | const keys: string[] = Object.keys(node); 114 | 115 | if (keys.indexOf('error') !== -1) { 116 | return tap(node.error, (fault: FaultNode) => (fault.type = this.parseErrorType(fault))); 117 | } 118 | 119 | if (keys.indexOf('warning') !== -1) { 120 | return tap(node.warning, (fault: FaultNode) => (fault.type = Type.WARNING)); 121 | } 122 | 123 | if (keys.indexOf('failure') !== -1) { 124 | return tap(node.failure, (fault: FaultNode) => (fault.type = Type.FAILURE)); 125 | } 126 | 127 | if (keys.indexOf('skipped') !== -1) { 128 | return { 129 | type: Type.SKIPPED, 130 | _type: 'PHPUnit\\Framework\\SkippedTestError', 131 | __text: 'Skipped Test', 132 | }; 133 | } 134 | 135 | if (keys.indexOf('incomplete') !== -1) { 136 | return { 137 | type: Type.INCOMPLETE, 138 | _type: 'PHPUnit\\Framework\\IncompleteTestError', 139 | __text: 'Incomplete Test', 140 | }; 141 | } 142 | 143 | return; 144 | } 145 | 146 | private parseErrorType(fault: FaultNode): Type { 147 | const type = fault._type.toLowerCase(); 148 | 149 | return ( 150 | [Type.WARNING, Type.FAILURE, Type.INCOMPLETE, Type.RISKY, Type.SKIPPED, Type.FAILED].find( 151 | errorType => type.indexOf(errorType) !== -1 152 | ) || Type.ERROR 153 | ); 154 | } 155 | 156 | private async parseDetails(fault: FaultNode): Promise { 157 | return Promise.all(fault.__text 158 | .split(/\r?\n/) 159 | .map((line: string) => line.trim()) 160 | .filter((line: string) => this.pathPattern.test(line)) 161 | .map(async (detail: string) => { 162 | const [, file, line] = detail.match(this.pathPattern) as string[]; 163 | 164 | return this.createLocation(file.trim(), parseInt(line, 10)); 165 | }) as Promise[]); 166 | } 167 | 168 | private current(details: Detail[], test: Test): Detail { 169 | return ( 170 | details.find(detail => test.uri === detail.uri && test.range.start.line !== detail.range.start.line) || test 171 | ); 172 | } 173 | 174 | private parseMessage(fault: any) { 175 | const messages: string[] = fault.__text 176 | .replace(/\r?\n/g, '\n') 177 | .replace(/ /g, '') 178 | .split('\n') 179 | .map((line: string) => line.replace(this.pathPattern, '')); 180 | 181 | return value(messages.slice(messages.length === 1 ? 0 : 1).join('\n'), (message: string) => { 182 | const type: string = fault._type || ''; 183 | 184 | return ( 185 | decode( 186 | /phpunit/i.test(type) 187 | ? message.replace(new RegExp(`^${type.replace(/\\/g, '\\\\')}:`), '') 188 | : message 189 | ).trim() || type 190 | ); 191 | }); 192 | } 193 | 194 | private filterDetails(details: Detail[], current: Detail): Detail[] { 195 | return details.filter( 196 | detail => detail.uri !== current.uri && detail.range.start.line !== current.range.start.line 197 | ); 198 | } 199 | 200 | private async createLocation(file: string, line: number): Promise { 201 | const uri = this.files.uri(file); 202 | 203 | return { 204 | uri: uri, 205 | range: await this.textLineRange.create(uri, line - 1), 206 | }; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/phpunit/parameters.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem, FilesystemContract } from '../filesystem'; 2 | import { tap } from '../helpers'; 3 | 4 | export class Parameters { 5 | private arguments: string[] = []; 6 | private cwd: string = ''; 7 | private root: string = ''; 8 | private jUnitDotXml: string = ''; 9 | 10 | constructor(private files: FilesystemContract = new Filesystem()) {} 11 | 12 | set(args: string[]): Parameters { 13 | return tap(this, (self: Parameters) => { 14 | self.arguments = args; 15 | }); 16 | } 17 | 18 | get(property: string): any { 19 | const index: number = this.arguments.indexOf(property); 20 | 21 | if (index !== -1) { 22 | if (index === this.arguments.length - 1) { 23 | return true; 24 | } 25 | 26 | const value: string = this.arguments[index + 1]; 27 | 28 | return value.indexOf('-') === 0 ? true : value; 29 | } 30 | 31 | return false; 32 | } 33 | 34 | exists(property: string): boolean { 35 | return this.arguments.some((arg: string) => property === arg); 36 | } 37 | 38 | setCwd(cwd: string): Parameters { 39 | return tap(this, (self: Parameters) => { 40 | self.cwd = cwd; 41 | }); 42 | } 43 | 44 | setRoot(root: string): Parameters { 45 | return tap(this, (self: Parameters) => { 46 | self.root = root; 47 | }); 48 | } 49 | 50 | async all(): Promise { 51 | const phpUnitDotXml: string = await this.findPhpUnitDotXml(); 52 | 53 | if (phpUnitDotXml) { 54 | this.arguments = this.arguments.concat(['-c', phpUnitDotXml]); 55 | } 56 | 57 | if (this.exists('--log-junit') === false) { 58 | this.jUnitDotXml = this.files.tmpfile('xml', 'phpunit-lsp'); 59 | this.arguments = this.arguments.concat(['--log-junit', this.jUnitDotXml]); 60 | } 61 | 62 | return this.arguments; 63 | } 64 | 65 | private async findPhpUnitDotXml(): Promise { 66 | if (this.exists('-c') === true || this.exists('--configuration') === true) { 67 | return ''; 68 | } 69 | 70 | return ( 71 | (await this.files.findUp('phpunit.xml', this.cwd, this.root)) || 72 | (await this.files.findUp('phpunit.xml.dist', this.cwd, this.root)) 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/phpunit/phpunit.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'vscode-languageserver-types'; 2 | import { Filesystem, FilesystemContract } from '../filesystem'; 3 | import { JUnit } from './junit'; 4 | import { os, OS, tap, value } from '../helpers'; 5 | import { Parameters } from './parameters'; 6 | import { Process } from '../process'; 7 | import { Test } from './common'; 8 | 9 | export class PhpUnit { 10 | private binary: string = ''; 11 | private defaults: string[] = []; 12 | private output: string = ''; 13 | private tests: Test[] = []; 14 | 15 | constructor( 16 | private files: FilesystemContract = new Filesystem(), 17 | private process: Process = new Process(), 18 | private parameters = new Parameters(files), 19 | private jUnit: JUnit = new JUnit() 20 | ) {} 21 | 22 | setBinary(binary: string): PhpUnit { 23 | return tap(this, (phpUnit: PhpUnit) => { 24 | phpUnit.binary = binary; 25 | }); 26 | } 27 | 28 | setDefault(args: string[]): PhpUnit { 29 | return tap(this, (phpUnit: PhpUnit) => { 30 | phpUnit.defaults = args; 31 | }); 32 | } 33 | 34 | async run(path: string, params: string[] = [], cwd: string = process.cwd()): Promise { 35 | if (path) { 36 | path = this.files.normalizePath(path); 37 | cwd = this.files.dirname(path); 38 | } 39 | const root: string = await this.getRoot(cwd); 40 | 41 | const command: Command = { 42 | title: '', 43 | command: await this.getBinary(cwd, root), 44 | arguments: await this.parameters 45 | .setCwd(cwd) 46 | .setRoot(root) 47 | .set(this.defaults.concat(params.concat([path]).filter((item: string) => !!item))) 48 | .all(), 49 | }; 50 | 51 | this.output = await this.process.spawn(command); 52 | this.tests = await this.jUnit.parseFile(this.parameters.get('--log-junit')); 53 | 54 | return 0; 55 | } 56 | 57 | getOutput(): string { 58 | return this.output; 59 | } 60 | 61 | getTests(): Test[] { 62 | return this.tests; 63 | } 64 | 65 | private async getRoot(cwd: string): Promise { 66 | return value(await this.files.findUp('composer.json', cwd), (composerPath: string) => { 67 | return composerPath ? this.files.dirname(composerPath) : cwd; 68 | }); 69 | } 70 | 71 | private async getBinary(cwd: string, root: string): Promise { 72 | if (this.binary) { 73 | return this.binary; 74 | } 75 | 76 | return ( 77 | (await this.files.findUp(`vendor/bin/phpunit${os() === OS.WIN ? '.bat' : ''}`, cwd, root)) || 78 | (await this.files.which('phpunit')) 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/phpunit/textline-range.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem, FilesystemContract } from '../filesystem'; 2 | import { Range } from 'vscode-languageserver-types'; 3 | 4 | export class TextlineRange { 5 | private items: Map = new Map(); 6 | 7 | constructor(private files: FilesystemContract = new Filesystem()) {} 8 | 9 | async create(uri: string, lineAt: number): Promise { 10 | return this.createRange(await this.getLines(uri), lineAt); 11 | } 12 | 13 | async findMethod(uri: string, lineAt: number): Promise { 14 | const lines: string[] = await this.getLines(uri); 15 | 16 | while (lineAt > 0) { 17 | const line: string = lines[lineAt]; 18 | const match = line.match(/^\s*(?:public|private|protected)?\s*function\s*(\w+)\s*\(.*$/); 19 | if (match) { 20 | return match[1]; 21 | } 22 | lineAt = lineAt - 1; 23 | } 24 | 25 | return ''; 26 | } 27 | 28 | clear(): TextlineRange { 29 | this.items.clear(); 30 | 31 | return this; 32 | } 33 | 34 | private createRange(lines: string[], lineAt: number): Range { 35 | const line: string = lines[lineAt]; 36 | const firstNonWhitespaceCharacterIndex: number = line.search(/\S|$/); 37 | 38 | return Range.create( 39 | { 40 | line: lineAt, 41 | character: firstNonWhitespaceCharacterIndex, 42 | }, 43 | { 44 | line: lineAt, 45 | character: firstNonWhitespaceCharacterIndex + line.trim().length, 46 | } 47 | ); 48 | } 49 | 50 | private async getLines(uri: string): Promise { 51 | if (this.items.has(uri) === false) { 52 | const content: string = await this.files.get(uri); 53 | this.items.set(uri, content.split(/\r?\n/g)); 54 | } 55 | 56 | return this.items.get(uri) || []; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/process.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn, SpawnOptions } from 'child_process'; 2 | import { Command } from 'vscode-languageserver-types'; 3 | 4 | export class Process { 5 | spawn(command: Command, options?: SpawnOptions): Promise { 6 | return new Promise(resolve => { 7 | const process: ChildProcess = spawn(command.command, command.arguments as string[], options); 8 | const output: Buffer[] = []; 9 | 10 | process.stdout.on('data', (buffer: Buffer) => { 11 | output.push(buffer); 12 | }); 13 | 14 | process.stderr.on('data', (buffer: Buffer) => { 15 | output.push(buffer); 16 | }); 17 | 18 | process.on('exit', () => { 19 | resolve( 20 | output 21 | .map(buffer => buffer.toString()) 22 | .join('') 23 | .replace(/\r?\n$/, '') 24 | ); 25 | }); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/codelens-provider.ts: -------------------------------------------------------------------------------- 1 | import { CodeLens, TextDocument } from 'vscode-languageserver-types'; 2 | import { Runner } from '../runner'; 3 | 4 | export class CodeLensProvider { 5 | constructor(private runner: Runner) {} 6 | 7 | provideCodeLenses(textDocument: TextDocument): CodeLens[] { 8 | return this.runner.getCodeLens(textDocument); 9 | } 10 | 11 | resolveCodeLens(codeLens: CodeLens): Promise { 12 | return new Promise(resolve => { 13 | resolve(codeLens); 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/providers/document-symbol-provider.ts: -------------------------------------------------------------------------------- 1 | import { Runner } from '../runner'; 2 | import { SymbolInformation, SymbolKind, TextDocument } from 'vscode-languageserver-types'; 3 | import { TestNode } from '../phpunit'; 4 | 5 | export class DocumentSymbolProvider { 6 | constructor(private runner = new Runner()) {} 7 | 8 | provideDocumentSymbols(textDocument: TextDocument): SymbolInformation[] { 9 | return this.convertToDocumentSymbol( 10 | this.runner.getTestNodes(textDocument.getText(), textDocument.uri), 11 | textDocument.uri 12 | ); 13 | } 14 | 15 | protected convertToDocumentSymbol(nodes: TestNode[], uri: string): SymbolInformation[] { 16 | return nodes.map((node: any) => { 17 | return SymbolInformation.create( 18 | node.name.replace(/.*\\/g, ''), 19 | node.class === node.name ? SymbolKind.Class : SymbolKind.Method, 20 | node.range, 21 | uri 22 | ); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './codelens-provider'; 2 | export * from './document-symbol-provider'; 3 | -------------------------------------------------------------------------------- /src/runner.ts: -------------------------------------------------------------------------------- 1 | import { Ast, Collection, PhpUnit, TestNode } from './phpunit'; 2 | import { CodeLens, Diagnostic, TextDocument } from 'vscode-languageserver-types'; 3 | import { Filesystem, FilesystemContract } from './filesystem'; 4 | import { IConnection } from 'vscode-languageserver'; 5 | import { tap, when } from './helpers'; 6 | import { TextlineRange } from './phpunit/textline-range'; 7 | 8 | export class Runner { 9 | constructor( 10 | private phpUnit: PhpUnit = new PhpUnit(), 11 | private collect: Collection = new Collection(), 12 | private ast: Ast = new Ast(), 13 | private files: FilesystemContract = new Filesystem(), 14 | private textlineRange: TextlineRange = new TextlineRange() 15 | ) {} 16 | 17 | setBinary(binary: string): Runner { 18 | return tap(this, () => { 19 | this.phpUnit.setBinary(binary); 20 | }); 21 | } 22 | 23 | setDefault(args: string[]): Runner { 24 | return tap(this, () => { 25 | this.phpUnit.setDefault(args); 26 | }); 27 | } 28 | 29 | async run(connection: IConnection, uri: string, path: string, params: string[] = []): Promise { 30 | await this.phpUnit.run(path, params); 31 | this.collect.put(this.phpUnit.getTests()); 32 | this.sendDiagnostics(connection).sendNotification(connection, uri); 33 | connection.console.log(this.phpUnit.getOutput()); 34 | 35 | return this; 36 | } 37 | 38 | async runNearest(connection: IConnection, uri: string, path: string, params: string[] = []) { 39 | const filter: string[] = this.getMethodFilter( 40 | await this.textlineRange.findMethod(path, parseInt(params[0], 10)) 41 | ); 42 | 43 | return tap(await this.run(connection, uri, path, filter), () => this.textlineRange.clear()); 44 | } 45 | 46 | getTestNodes(code: string, uri: string): TestNode[] { 47 | return this.ast.parse(code, this.files.uri(uri)); 48 | } 49 | 50 | sendDiagnostics(connection: IConnection): Runner { 51 | return tap(this, () => { 52 | this.collect.getDiagnoics().forEach((diagnostics: Diagnostic[], uri: string) => { 53 | connection.sendDiagnostics({ 54 | uri, 55 | diagnostics, 56 | }); 57 | }); 58 | }); 59 | } 60 | 61 | sendNotification(connection: IConnection, pathOrUri: string): Runner { 62 | return tap(this, () => { 63 | const uri: string = this.files.uri(pathOrUri); 64 | connection.sendNotification('assertions', { 65 | uri: uri, 66 | assertions: this.collect.getAssertions().get(uri) || [], 67 | }); 68 | }); 69 | } 70 | 71 | getCodeLens(textDocument: TextDocument): CodeLens[] { 72 | const uri: string = textDocument.uri; 73 | const testNodes: TestNode[] = this.getTestNodes(textDocument.getText(), uri); 74 | 75 | return testNodes 76 | .concat( 77 | this.collect.getTestNodes(uri).filter((testNode: TestNode) => { 78 | for (const node of testNodes) { 79 | if (node.uri === testNode.uri && testNode.range === testNode.range) { 80 | return false; 81 | } 82 | } 83 | 84 | return true; 85 | }) 86 | ) 87 | .map((node: TestNode) => this.transformTestNodeToCodeLen(node, uri)); 88 | } 89 | 90 | private transformTestNodeToCodeLen(node: TestNode, uri: string) { 91 | return { 92 | range: node.range, 93 | command: when( 94 | node.class === node.name, 95 | () => { 96 | return { 97 | title: 'Run Test', 98 | command: 'phpunit.test.file', 99 | arguments: [uri, this.files.normalizePath(node.uri), []], 100 | }; 101 | }, 102 | () => { 103 | return { 104 | title: 'Run Test', 105 | command: 'phpunit.test', 106 | arguments: [uri, this.files.normalizePath(node.uri), this.getMethodFilter(node.name)], 107 | }; 108 | } 109 | ), 110 | data: { 111 | textDocument: { 112 | uri: uri, 113 | }, 114 | }, 115 | }; 116 | } 117 | 118 | private getMethodFilter(method: string): string[] { 119 | return method ? ['--filter', `^.*::${method}$`] : []; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 'use strict'; 6 | 7 | import { CodeLensProvider, DocumentSymbolProvider } from './providers'; 8 | import { Runner } from './runner'; 9 | import { 10 | CodeLens, 11 | CodeLensParams, 12 | IConnection, 13 | InitializeResult, 14 | TextDocuments, 15 | createConnection, 16 | ExecuteCommandParams, 17 | DocumentSymbolParams, 18 | SymbolInformation, 19 | DidChangeConfigurationParams, 20 | ProposedFeatures, 21 | } from 'vscode-languageserver'; 22 | 23 | const runner: Runner = new Runner(); 24 | const documentSymbolProvider: DocumentSymbolProvider = new DocumentSymbolProvider(runner); 25 | const codeLensProvider: CodeLensProvider = new CodeLensProvider(runner); 26 | 27 | // Create a connection for the server. The connection uses Node's IPC as a transport. 28 | // Also include all preview / proposed LSP features. 29 | const connection: IConnection = createConnection(ProposedFeatures.all); 30 | 31 | // Create a simple text document manager. The text document manager 32 | // supports full document sync only 33 | const documents: TextDocuments = new TextDocuments(); 34 | // Make the text document manager listen on the connection 35 | // for open, change and close text document events 36 | documents.listen(connection); 37 | 38 | // After the server has started the client sends an initialize request. The server receives 39 | // in the passed params the rootPath of the workspace plus the client capabilities. 40 | // connection.onInitialize((params: InitializeParams): InitializeResult => { 41 | connection.onInitialize((): InitializeResult => { 42 | return { 43 | capabilities: { 44 | // Tell the client that the server works in FULL text document sync mode 45 | textDocumentSync: documents.syncKind, 46 | // Tell the client that the server support code complete 47 | // completionProvider: { 48 | // resolveProvider: true, 49 | // }, 50 | codeLensProvider: { 51 | resolveProvider: true, 52 | }, 53 | documentSymbolProvider: false, 54 | executeCommandProvider: { 55 | commands: ['phpunit.test', 'phpunit.test.file', 'phpunit.test.suite', 'phpunit.test.nearest'], 56 | }, 57 | }, 58 | }; 59 | }); 60 | 61 | // The content of a text document has changed. This event is emitted 62 | // when the text document first opened or when its content has changed. 63 | // documents.onDidChangeContent(change => { 64 | // validateTextDocument(change.document); 65 | // }); 66 | 67 | // The settings interface describe the server relevant settings part 68 | interface Settings { 69 | phpunit: PhpUnitSettings; 70 | } 71 | 72 | // These are the example settings we defined in the client's package.json 73 | // file 74 | interface PhpUnitSettings { 75 | execPath: string; 76 | args: string[]; 77 | } 78 | 79 | // hold the maxNumberOfProblems setting 80 | // The settings have changed. Is send on server activation 81 | // as well. 82 | connection.onDidChangeConfiguration((change: DidChangeConfigurationParams) => { 83 | const settings = change.settings as Settings; 84 | runner.setBinary(settings.phpunit.execPath).setDefault(settings.phpunit.args); 85 | }); 86 | 87 | connection.onDidChangeWatchedFiles(() => { 88 | // Monitored files have change in VSCode 89 | connection.console.log('We received an file change event'); 90 | }); 91 | 92 | // This handler provides the initial list of the completion items. 93 | // connection.onCompletion((_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => { 94 | // // The pass parameter contains the position of the text document in 95 | // // which code complete got requested. For the example we ignore this 96 | // // info and always provide the same completion items. 97 | // return [ 98 | // { 99 | // label: 'TypeScript', 100 | // kind: CompletionItemKind.Text, 101 | // data: 1, 102 | // }, 103 | // { 104 | // label: 'JavaScript', 105 | // kind: CompletionItemKind.Text, 106 | // data: 2, 107 | // }, 108 | // ]; 109 | // }); 110 | 111 | // This handler resolve additional information for the item selected in 112 | // the completion list. 113 | // connection.onCompletionResolve((item: CompletionItem): CompletionItem => { 114 | // if (item.data === 1) { 115 | // (item.detail = 'TypeScript details'), (item.documentation = 'TypeScript documentation'); 116 | // } else if (item.data === 2) { 117 | // (item.detail = 'JavaScript details'), (item.documentation = 'JavaScript documentation'); 118 | // } 119 | 120 | // return item; 121 | // }); 122 | 123 | connection.onCodeLens((params: CodeLensParams): CodeLens[] => { 124 | return codeLensProvider.provideCodeLenses(documents.get(params.textDocument.uri)); 125 | }); 126 | 127 | connection.onCodeLensResolve((codeLens: CodeLens) => { 128 | return codeLensProvider.resolveCodeLens(codeLens); 129 | }); 130 | 131 | connection.onExecuteCommand(async (params: ExecuteCommandParams) => { 132 | const p: any = params.arguments || []; 133 | const uri: string = p[0] || ''; 134 | const path: string = p[1] || ''; 135 | const args: string[] = p[2] || []; 136 | 137 | switch (params.command) { 138 | case 'phpunit.test': 139 | case 'phpunit.test.file': 140 | await runner.run(connection, uri, path, args); 141 | break; 142 | 143 | case 'phpunit.test.suite': 144 | await runner.run(connection, '', path, args); 145 | break; 146 | 147 | case 'phpunit.test.nearest': 148 | await runner.runNearest(connection, uri, path, args); 149 | break; 150 | } 151 | }); 152 | 153 | connection.onRequest('assertions', params => { 154 | runner.sendNotification(connection, params.uri); 155 | }); 156 | 157 | connection.onDocumentSymbol((params: DocumentSymbolParams): SymbolInformation[] => { 158 | return documentSymbolProvider.provideDocumentSymbols(documents.get(params.textDocument.uri)); 159 | }); 160 | 161 | // connection.onDidSaveTextDocument(async params => { 162 | // const uri: string = params.textDocument.uri; 163 | // await phpUnit.run(uri); 164 | // phpUnit.sendDiagnostics(connection).sendNotification(connection, uri); 165 | // connection.console.log(phpUnit.getOutput()); 166 | // }); 167 | 168 | /* 169 | connection.onDidOpenTextDocument((params) => { 170 | // A text document got opened in VSCode. 171 | // params.uri uniquely identifies the document. For documents store on disk this is a file URI. 172 | // params.text the initial full content of the document. 173 | connection.console.log(`${params.textDocument.uri} opened.`); 174 | }); 175 | connection.onDidChangeTextDocument((params) => { 176 | // The content of a text document did change in VSCode. 177 | // params.uri uniquely identifies the document. 178 | // params.contentChanges describe the content changes to the document. 179 | connection.console.log(`${params.textDocument.uri} changed: ${JSON.stringify(params.contentChanges)}`); 180 | }); 181 | connection.onDidCloseTextDocument((params) => { 182 | // A text document got closed in VSCode. 183 | // params.uri uniquely identifies the document. 184 | connection.console.log(`${params.textDocument.uri} closed.`); 185 | }); 186 | */ 187 | 188 | // Listen on the connection 189 | connection.listen(); 190 | -------------------------------------------------------------------------------- /tests/filesystem/adapter.test.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem, FilesystemContract } from '../../src/filesystem'; 2 | import { OS, os } from '../../src/helpers'; 3 | import { readFileSync, writeFileSync } from 'fs'; 4 | import { resolve } from 'path'; 5 | import { tmpdir } from 'os'; 6 | 7 | function toFileUrl(path: string): string { 8 | return path.replace(/\\/g, '/').replace(/^(\w):/i, m => { 9 | return `file:///${m[0].toLowerCase()}%3A`; 10 | }); 11 | } 12 | 13 | describe('Filesystem Test', () => { 14 | it('it should get content from file', async () => { 15 | const files: FilesystemContract = new Filesystem(); 16 | const path = resolve(__dirname, '../fixtures/project/tests/AssertionsTest.php'); 17 | expect(await files.get(path)).toEqual(readFileSync(path).toString('utf8')); 18 | }); 19 | 20 | it('it should get content from file url', async () => { 21 | const files: FilesystemContract = new Filesystem(); 22 | const path = resolve(__dirname, '../fixtures/project/tests/AssertionsTest.php'); 23 | expect(await files.get(toFileUrl(path))).toEqual(readFileSync(path).toString('utf8')); 24 | }); 25 | 26 | it('check file exists', async () => { 27 | const files: FilesystemContract = new Filesystem(); 28 | 29 | expect(await files.exists(resolve(__dirname, '../fixtures/bin/ls'))).toBeTruthy(); 30 | expect(await files.exists(resolve(__dirname, '../fixtures/bin/cmd.exe'))).toBeTruthy(); 31 | expect(await files.exists(resolve(__dirname, '../fixtures/bin/pwd'))).toBeFalsy(); 32 | }); 33 | 34 | it('check file url exists', async () => { 35 | const files: FilesystemContract = new Filesystem(); 36 | 37 | expect(await files.exists(toFileUrl(resolve(__dirname, '../fixtures/bin/ls')))).toBeTruthy(); 38 | expect(await files.exists(toFileUrl(resolve(__dirname, '../fixtures/bin/cmd.exe')))).toBeTruthy(); 39 | expect(await files.exists(toFileUrl(resolve(__dirname, '../fixtures/bin/pwd')))).toBeFalsy(); 40 | }); 41 | 42 | it('it should find path when path not include path', async () => { 43 | const files: FilesystemContract = new Filesystem(); 44 | const systemPaths = [resolve(__dirname, '../fixtures/bin'), resolve(__dirname, '../fixtures/usr/bin')]; 45 | 46 | if (os() === OS.WIN) { 47 | files.setSystemPaths(systemPaths.join(';')); 48 | expect(await files.which('windows.test.ts', __dirname)).toEqual(resolve(__dirname, 'windows.test.ts')); 49 | expect(await files.which('cmd.exe')).toEqual(resolve(__dirname, '../fixtures/bin/cmd.exe')); 50 | expect(await files.which('cmd')).toEqual(resolve(__dirname, '../fixtures/bin/cmd.exe')); 51 | expect(await files.which('ls')).toEqual(resolve(__dirname, '../fixtures/bin/ls')); 52 | } else { 53 | files.setSystemPaths(systemPaths.join(':')); 54 | expect(await files.which('posix.test.ts', __dirname)).toEqual(resolve(__dirname, 'posix.test.ts')); 55 | expect(await files.which('cmd.exe')).toEqual(resolve(__dirname, '../fixtures/bin/cmd.exe')); 56 | expect(await files.which('cmd')).toEqual(resolve(__dirname, '../fixtures/bin/cmd')); 57 | expect(await files.which('ls')).toEqual(resolve(__dirname, '../fixtures/bin/ls')); 58 | } 59 | }); 60 | 61 | it('it should find up path', async () => { 62 | const files: FilesystemContract = new Filesystem(); 63 | const root: string = resolve(__dirname, '../fixtures'); 64 | expect(await files.findUp('vendor/bin/phpunit', resolve(__dirname, '../fixtures/project/tests'), root)).toEqual( 65 | resolve(__dirname, '../fixtures/project/vendor/bin/phpunit') 66 | ); 67 | 68 | expect(await files.findUp('vendor/bin/phpunit', resolve(__dirname, '../fixtures/project'), root)).toEqual( 69 | resolve(__dirname, '../fixtures/project/vendor/bin/phpunit') 70 | ); 71 | 72 | expect(await files.findUp('vendor/bin/phpunit1', resolve(__dirname, '../fixtures/project'), root)).toEqual(''); 73 | }); 74 | 75 | it('it should return random file name with extension', () => { 76 | const files: FilesystemContract = new Filesystem(); 77 | const dir: string = tmpdir(); 78 | expect(files.tmpfile('php', 'test')).toMatch( 79 | new RegExp(`${resolve(dir, 'test-').replace(/\\/g, '\\\\')}\\d+\.php$`) 80 | ); 81 | }); 82 | 83 | it('it should delete file', async () => { 84 | const files: FilesystemContract = new Filesystem(); 85 | const path = resolve(__dirname, 'unlink.txt'); 86 | writeFileSync(path, 'unlink'); 87 | 88 | expect(await files.exists(path)).toBeTruthy(); 89 | await files.unlink(path); 90 | expect(await files.exists(path)).toBeFalsy(); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/filesystem/posix.test.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem, FilesystemContract, POSIX } from '../../src/filesystem'; 2 | import { join, resolve } from 'path'; 3 | import { OS, os } from '../../src/helpers'; 4 | import { readFileSync } from 'fs'; 5 | import { spawnSync } from 'child_process'; 6 | 7 | describe('POSIX Filesystem Test', () => { 8 | it('it should normalize path', () => { 9 | const files: FilesystemContract = new POSIX(); 10 | expect(files.normalizePath('file:///foo/bar')).toEqual('/foo/bar'); 11 | expect(files.normalizePath('file:///foo/ba r')).toEqual('/foo/ba\\ r'); 12 | }); 13 | 14 | it('it should normalize path with adapter', () => { 15 | const files: FilesystemContract = new Filesystem(new POSIX()); 16 | expect(files.normalizePath('file:///foo/bar')).toEqual('/foo/bar'); 17 | }); 18 | 19 | it('it should receive paths from system', () => { 20 | const files: FilesystemContract = new POSIX(); 21 | const systemPaths = ['/bin', '/usr/bin', '/usr/local/bin']; 22 | 23 | if (os() === OS.POSIX) { 24 | expect(files.getSystemPaths().join(':')).toEqual(process.env.PATH as string); 25 | } 26 | 27 | files.setSystemPaths(systemPaths.join(':')); 28 | expect(files.getSystemPaths()).toEqual(systemPaths); 29 | }); 30 | 31 | it('it should find path when path not include path', async () => { 32 | const files: FilesystemContract = new POSIX(); 33 | const systemPaths = [resolve(__dirname, '../fixtures/bin'), resolve(__dirname, '../fixtures/usr/bin')]; 34 | files.setSystemPaths(systemPaths.join(':')); 35 | 36 | expect(await files.which('posix.test.ts', __dirname)).toEqual(resolve(__dirname, 'posix.test.ts')); 37 | expect(await files.which('cmd.exe')).toEqual(resolve(__dirname, '../fixtures/bin/cmd.exe')); 38 | expect(await files.which('cmd')).toEqual(resolve(__dirname, '../fixtures/bin/cmd')); 39 | expect(await files.which('ls')).toEqual(resolve(__dirname, '../fixtures/bin/ls')); 40 | }); 41 | 42 | it('it should find up path', async () => { 43 | const files: FilesystemContract = new POSIX(); 44 | expect(await files.findUp('vendor/bin/phpunit', resolve(__dirname, '../fixtures/project/tests'))).toEqual( 45 | resolve(__dirname, '../fixtures/project/vendor/bin/phpunit') 46 | ); 47 | 48 | expect(await files.findUp('vendor/bin/phpunit', resolve(__dirname, '../fixtures/project'))).toEqual( 49 | resolve(__dirname, '../fixtures/project/vendor/bin/phpunit') 50 | ); 51 | }); 52 | 53 | it('it should convert file to uri', () => { 54 | if (os() === OS.WIN) { 55 | return; 56 | } 57 | 58 | const files: FilesystemContract = new Filesystem(); 59 | expect(files.uri('/foo/bar')).toEqual('file:///foo/bar'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/filesystem/windows.test.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem, FilesystemContract, WINDOWS } from '../../src/filesystem'; 2 | import { OS, os } from '../../src/helpers'; 3 | import { resolve } from 'path'; 4 | 5 | describe('Windows Filesystem Test', () => { 6 | it('it should normalize path', () => { 7 | const files: FilesystemContract = new WINDOWS(); 8 | expect(files.normalizePath('file:///c%3A/foo/bar')).toEqual('c:\\foo\\bar'); 9 | expect(files.normalizePath('file:///c:/foo/bar')).toEqual('c:\\foo\\bar'); 10 | expect(files.normalizePath('c:\\foo\\bar')).toEqual('c:\\foo\\bar'); 11 | expect(files.normalizePath('c:/foo/bar')).toEqual('c:\\foo\\bar'); 12 | expect(files.normalizePath('file:///c%3A/foo/ba r')).toEqual('c:\\foo\\ba\\ r'); 13 | }); 14 | 15 | it('it should normalize path with adapter', () => { 16 | const files: FilesystemContract = new Filesystem(new WINDOWS()); 17 | expect(files.normalizePath('file:///c%3A/foo/bar')).toEqual('c:\\foo\\bar'); 18 | expect(files.normalizePath('file:///c:/foo/bar')).toEqual('c:\\foo\\bar'); 19 | }); 20 | 21 | it('it should receive paths from system', () => { 22 | const files: FilesystemContract = new WINDOWS(); 23 | const systemPaths = ['C:\\WINDOWS\\', 'C:\\Program\\', 'C:\\ProgramData\\']; 24 | 25 | if (os() === OS.WIN) { 26 | expect(files.getSystemPaths().join(';')).toEqual(process.env.PATH as string); 27 | } 28 | 29 | files.setSystemPaths(systemPaths.join(';')); 30 | expect(files.getSystemPaths()).toEqual(systemPaths); 31 | }); 32 | 33 | it('it should find path when path not include path', async () => { 34 | if (os() !== OS.WIN) { 35 | return; 36 | } 37 | 38 | const files: FilesystemContract = new WINDOWS(); 39 | const systemPaths = [resolve(__dirname, '../fixtures/bin'), resolve(__dirname, '../fixtures/usr/bin')]; 40 | files.setSystemPaths(systemPaths.join(';')); 41 | 42 | expect(await files.which('windows.test.ts', __dirname)).toEqual(resolve(__dirname, 'windows.test.ts')); 43 | expect(await files.which('cmd.exe')).toEqual(resolve(__dirname, '../fixtures/bin/cmd.exe')); 44 | expect(await files.which('cmd')).toEqual(resolve(__dirname, '../fixtures/bin/cmd.exe')); 45 | expect(await files.which('ls')).toEqual(resolve(__dirname, '../fixtures/bin/ls')); 46 | }); 47 | 48 | it('it should find up path', async () => { 49 | if (os() !== OS.WIN) { 50 | return; 51 | } 52 | 53 | const files: FilesystemContract = new WINDOWS(); 54 | expect(await files.findUp('vendor/bin/phpunit', resolve(__dirname, '../fixtures/project/tests'))).toEqual( 55 | resolve(__dirname, '../fixtures/project/vendor/bin/phpunit') 56 | ); 57 | 58 | expect(await files.findUp('vendor/bin/phpunit', resolve(__dirname, '../fixtures/project'))).toEqual( 59 | resolve(__dirname, '../fixtures/project/vendor/bin/phpunit') 60 | ); 61 | }); 62 | 63 | it('it should convert file to uri', () => { 64 | if (os() !== OS.WIN) { 65 | return; 66 | } 67 | 68 | const files: FilesystemContract = new Filesystem(); 69 | expect(files.uri('C:\\foo\\bar')).toEqual('file:///c%3A/foo/bar'); 70 | expect(files.uri('C:\\foo\\ba r')).toEqual('file:///c%3A/foo/ba%20r'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/fixtures/PHPUnitTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 16 | } 17 | 18 | public function testFailed() 19 | { 20 | $this->assertTrue(false); 21 | } 22 | 23 | public function testSkipped() 24 | { 25 | $this->markTestSkipped('The MySQLi extension is not available.'); 26 | } 27 | 28 | public function testIncomplete() 29 | { 30 | $this->markTestIncomplete('This test has not been implemented yet.'); 31 | } 32 | 33 | public function testNoAssertions() 34 | { 35 | 36 | } 37 | 38 | public function testAssertNotEquals() { 39 | $this->assertSame(['a' => 'b', 'c' => 'd'], ['e' => 'f', 'g', 'h']); 40 | } 41 | 42 | /** 43 | * @test 44 | * 45 | * @return void 46 | */ 47 | public function it_should_be_test_case() { 48 | 49 | } 50 | } -------------------------------------------------------------------------------- /tests/fixtures/bin/cmd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recca0120/phpunit-language-server/1c4b5e0d8801613fa363760fb61c2e2516a2e2d3/tests/fixtures/bin/cmd -------------------------------------------------------------------------------- /tests/fixtures/bin/cmd.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recca0120/phpunit-language-server/1c4b5e0d8801613fa363760fb61c2e2516a2e2d3/tests/fixtures/bin/cmd.exe -------------------------------------------------------------------------------- /tests/fixtures/bin/ls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recca0120/phpunit-language-server/1c4b5e0d8801613fa363760fb61c2e2516a2e2d3/tests/fixtures/bin/ls -------------------------------------------------------------------------------- /tests/fixtures/junit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PHPUnitTest::testFailed 7 | Failed asserting that false is true. 8 | 9 | C:\Users\recca\github\tester-phpunit\tests\PHPUnitTest.php:20 10 | 11 | 12 | 13 | PHPUnitTest::testError 14 | PHPUnit_Framework_Exception: Argument #1 (No Value) of PHPUnit_Framework_Assert::assertInstanceOf() must be a class or interface name 15 | 16 | C:\Users\recca\github\tester-phpunit\tests\PHPUnitTest.php:25 17 | 18 | 19 | 20 | Skipped Test 21 | C:\Users\recca\github\tester-phpunit\tests\PHPUnitTest.php:30 22 | 23 | 24 | 25 | Incomplete Test 26 | C:\Users\recca\github\tester-phpunit\tests\PHPUnitTest.php:35 27 | 28 | 29 | 30 | 31 | 32 | PHPUnitTest::testReceive 33 | BadMethodCallException: Method Mockery_1_Symfony_Component_HttpFoundation_File_UploadedFile::getClientOriginalName() does not exist on this mock object 34 | 35 | C:\Users\recca\github\tester-phpunit\src\Receiver.php:85 36 | C:\Users\recca\github\tester-phpunit\src\Receiver.php:68 37 | C:\Users\recca\github\tester-phpunit\tests\PHPUnitTest.php:45 38 | 39 | 40 | 41 | Recca0120\Upload\Tests\PHPUnitTest::testCleanDirectory 42 | Mockery\Exception\InvalidCountException: Method delete("C:\Users\recca\github\tester-phpunit\tests\PHPUnitTest.php") from Mockery_1_Recca0120_Upload_Filesystem should be called 43 | exactly 1 times but called 0 times. 44 | 45 | C:\Users\recca\UniServerZ\www\driways\laravel\vendor\mockery\mockery\library\Mockery\CountValidator\Exact.php:37 46 | C:\Users\recca\UniServerZ\www\driways\laravel\vendor\mockery\mockery\library\Mockery\Expectation.php:298 47 | C:\Users\recca\UniServerZ\www\driways\laravel\vendor\mockery\mockery\library\Mockery\ExpectationDirector.php:120 48 | C:\Users\recca\UniServerZ\www\driways\laravel\vendor\mockery\mockery\library\Mockery\Container.php:297 49 | C:\Users\recca\UniServerZ\www\driways\laravel\vendor\mockery\mockery\library\Mockery\Container.php:282 50 | C:\Users\recca\UniServerZ\www\driways\laravel\vendor\mockery\mockery\library\Mockery.php:152 51 | C:\Users\recca\github\tester-phpunit\tests\PHPUnitTest.php:13 52 | C:\ProgramData\ComposerSetup\vendor\phpunit\phpunit\src\TextUI\Command.php:188 53 | C:\ProgramData\ComposerSetup\vendor\phpunit\phpunit\src\TextUI\Command.php:118 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Risky Test 66 | C:\ProgramData\ComposerSetup\vendor\phpunit\phpunit\src\TextUI\Command.php:195 67 | C:\ProgramData\ComposerSetup\vendor\phpunit\phpunit\src\TextUI\Command.php:148 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /tests/fixtures/project/.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .php_cs.cache 3 | composer.lock -------------------------------------------------------------------------------- /tests/fixtures/project/.php_cs: -------------------------------------------------------------------------------- 1 | [ 8 | 'syntax' => 'short', 9 | ], 10 | 'binary_operator_spaces' => [ 11 | 'align_double_arrow' => false, 12 | 'align_equals' => false, 13 | ], 14 | 'blank_line_after_namespace' => true, 15 | 'blank_line_after_opening_tag' => true, 16 | 'blank_line_before_return' => true, 17 | 'braces' => true, 18 | 'cast_spaces' => true, 19 | 'class_definition' => true, 20 | 'concat_space' => [ 21 | 'spacing' => 'none', 22 | ], 23 | 'declare_equal_normalize' => true, 24 | 'elseif' => true, 25 | 'encoding' => true, 26 | 'full_opening_tag' => true, 27 | 'function_declaration' => true, 28 | 'function_typehint_space' => true, 29 | 'hash_to_slash_comment' => true, 30 | 'heredoc_to_nowdoc' => true, 31 | 'include' => true, 32 | 'lowercase_cast' => true, 33 | 'lowercase_constants' => true, 34 | 'lowercase_keywords' => true, 35 | 'method_argument_space' => true, 36 | 'method_separation' => true, 37 | 'native_function_casing' => true, 38 | 'no_alias_functions' => true, 39 | 'no_blank_lines_after_class_opening' => true, 40 | 'no_blank_lines_after_phpdoc' => true, 41 | 'no_closing_tag' => true, 42 | 'no_empty_phpdoc' => true, 43 | 'no_empty_statement' => true, 44 | 'no_extra_consecutive_blank_lines' => true, 45 | 'no_leading_import_slash' => true, 46 | 'no_leading_namespace_whitespace' => true, 47 | 'no_mixed_echo_print' => true, 48 | 'no_multiline_whitespace_around_double_arrow' => true, 49 | 'no_multiline_whitespace_before_semicolons' => true, 50 | 'no_short_bool_cast' => true, 51 | 'no_singleline_whitespace_before_semicolons' => true, 52 | 'no_spaces_after_function_name' => true, 53 | 'no_spaces_inside_parenthesis' => true, 54 | 'no_trailing_comma_in_list_call' => true, 55 | 'no_trailing_comma_in_singleline_array' => true, 56 | 'no_trailing_whitespace_in_comment' => true, 57 | 'no_trailing_whitespace' => true, 58 | 'no_unneeded_control_parentheses' => true, 59 | 'no_unreachable_default_argument_value' => true, 60 | 'no_unused_imports' => true, 61 | 'no_useless_return' => true, 62 | 'no_whitespace_before_comma_in_array' => true, 63 | 'no_whitespace_in_blank_line' => true, 64 | 'normalize_index_brace' => true, 65 | 'not_operator_with_successor_space' => true, 66 | 'object_operator_without_whitespace' => true, 67 | 'ordered_class_elements' => true, 68 | 'ordered_imports' => [ 69 | 'sortAlgorithm' => 'length', 70 | ], 71 | 'phpdoc_indent' => true, 72 | 'phpdoc_inline_tag' => true, 73 | 'phpdoc_no_access' => true, 74 | 'phpdoc_no_package' => true, 75 | 'phpdoc_no_useless_inheritdoc' => true, 76 | 'phpdoc_scalar' => true, 77 | 'phpdoc_single_line_var_spacing' => true, 78 | 'phpdoc_summary' => true, 79 | 'phpdoc_to_comment' => true, 80 | 'phpdoc_trim' => true, 81 | 'phpdoc_types' => true, 82 | 'phpdoc_var_without_name' => true, 83 | 'phpdoc_var_without_name' => true, 84 | 'psr4' => true, 85 | 'self_accessor' => true, 86 | 'short_scalar_cast' => true, 87 | 'simplified_null_return' => true, 88 | 'single_blank_line_at_eof' => true, 89 | 'single_blank_line_before_namespace' => true, 90 | 'single_class_element_per_statement' => true, 91 | 'single_import_per_statement' => true, 92 | 'single_line_after_imports' => true, 93 | 'single_quote' => true, 94 | 'space_after_semicolon' => true, 95 | 'standardize_not_equals' => true, 96 | 'switch_case_semicolon_to_colon' => true, 97 | 'switch_case_space' => true, 98 | 'ternary_operator_spaces' => true, 99 | 'trailing_comma_in_multiline_array' => true, 100 | 'trim_array_spaces' => true, 101 | 'unary_operator_spaces' => true, 102 | 'visibility_required' => [ 103 | 'method', 104 | 'property', 105 | ], 106 | 'whitespace_after_comma_in_array' => true, 107 | ]; 108 | 109 | return Config::create() 110 | ->setFinder(Finder::create()->in(__DIR__)) 111 | ->setRules($rules) 112 | ->setRiskyAllowed(true) 113 | ->setUsingCache(true); 114 | -------------------------------------------------------------------------------- /tests/fixtures/project/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "mockery/mockery": "~0.9.4|~1.0", 4 | "phpunit/phpunit": "~4.8|~5.4|~6.1|~7.0" 5 | }, 6 | "autoload": { 7 | "psr-4": { 8 | "App\\": "src/" 9 | } 10 | }, 11 | "autoload-dev": { 12 | "psr-4": { 13 | "Tests\\": "tests/" 14 | } 15 | }, 16 | "config": { 17 | "preferred-install": "dist", 18 | "sort-packages": true, 19 | "optimize-autoloader": true 20 | }, 21 | "minimum-stability": "dev", 22 | "prefer-stable": true 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/project/junit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tests\AssertionsTest::test_error 8 | Failed asserting that false is true. 9 | 10 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php:16 11 | 12 | 13 | 14 | Tests\AssertionsTest::test_assertion_isnt_same 15 | Failed asserting that two arrays are identical. 16 | --- Expected 17 | +++ Actual 18 | @@ @@ 19 | Array &0 ( 20 | - 'a' => 'b' 21 | - 'c' => 'd' 22 | + 'e' => 'f' 23 | + 0 => 'g' 24 | + 1 => 'h' 25 | ) 26 | 27 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php:21 28 | 29 | 30 | 31 | Risky Test 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Risky Test 43 | 44 | 45 | 46 | 47 | 48 | 49 | Tests\CalculatorTest::test_sum_fail 50 | Failed asserting that 4 is identical to 3. 51 | 52 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php:29 53 | 54 | 55 | 56 | 57 | Tests\CalculatorTest::test_sum_item_method_not_call 58 | Mockery\Exception\InvalidCountException: Method test(<Any Arguments>) from Mockery_0_App_Item_App_Item should be called 59 | exactly 1 times but called 0 times. 60 | 61 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\CountValidator\Exact.php:38 62 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\Expectation.php:309 63 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\ExpectationDirector.php:119 64 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\Container.php:301 65 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\Container.php:286 66 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery.php:165 67 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php:15 68 | 69 | 70 | 71 | Tests\CalculatorTest::test_throw_exception 72 | Exception: 73 | 74 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\src\Calculator.php:21 75 | C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php:57 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /tests/fixtures/project/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | 21 | 22 | ./src 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/fixtures/project/src/Calculator.php: -------------------------------------------------------------------------------- 1 | value() + $b->value(); 17 | } 18 | 19 | public function throwException() 20 | { 21 | throw new Exception; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/fixtures/project/src/Item.php: -------------------------------------------------------------------------------- 1 | value = $value; 12 | } 13 | 14 | public function value() 15 | { 16 | return $this->value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/fixtures/project/teamcity.txt: -------------------------------------------------------------------------------- 1 | PHPUnit 7.1.1 by Sebastian Bergmann and contributors. 2 | 3 | Runtime: PHP 7.1.7 4 | Configuration: C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\phpunit.xml.dist 5 | 6 | 7 | ##teamcity[testCount count='12' flowId='112040'] 8 | 9 | ##teamcity[testSuiteStarted name='Package Test Suite' flowId='112040'] 10 | 11 | ##teamcity[testSuiteStarted name='Tests\AssertionsTest' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest' flowId='112040'] 12 | 13 | ##teamcity[testStarted name='test_passed' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest::test_passed' flowId='112040'] 14 | 15 | ##teamcity[testFinished name='test_passed' duration='10' flowId='112040'] 16 | 17 | ##teamcity[testStarted name='test_error' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest::test_error' flowId='112040'] 18 | 19 | ##teamcity[testFailed name='test_error' message='Failed asserting that false is true.' details=' C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php:16|n ' flowId='112040'] 20 | 21 | ##teamcity[testFinished name='test_error' duration='0' flowId='112040'] 22 | 23 | ##teamcity[testStarted name='test_assertion_isnt_same' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest::test_assertion_isnt_same' flowId='112040'] 24 | 25 | ##teamcity[testFailed name='test_assertion_isnt_same' message='Failed asserting that two arrays are identical.' details=' C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php:21|n ' type='comparisonFailure' actual='Array &0 (|n |'e|' => |'f|'|n 0 => |'g|'|n 1 => |'h|'|n)' expected='Array &0 (|n |'a|' => |'b|'|n |'c|' => |'d|'|n)' flowId='112040'] 26 | 27 | ##teamcity[testFinished name='test_assertion_isnt_same' duration='0' flowId='112040'] 28 | 29 | ##teamcity[testStarted name='test_risky' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest::test_risky' flowId='112040'] 30 | 31 | ##teamcity[testFailed name='test_risky' message='This test did not perform any assertions' details=' ' flowId='112040'] 32 | 33 | ##teamcity[testFinished name='test_risky' duration='0' flowId='112040'] 34 | 35 | ##teamcity[testStarted name='it_should_be_annotation_test' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest::it_should_be_annotation_test' flowId='112040'] 36 | 37 | ##teamcity[testFinished name='it_should_be_annotation_test' duration='0' flowId='112040'] 38 | 39 | ##teamcity[testStarted name='test_skipped' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest::test_skipped' flowId='112040'] 40 | 41 | ##teamcity[testIgnored name='test_skipped' message='The MySQLi extension is not available.' details=' C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php:39|n ' flowId='112040'] 42 | 43 | ##teamcity[testFinished name='test_skipped' duration='0' flowId='112040'] 44 | 45 | ##teamcity[testStarted name='test_incomplete' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest::test_incomplete' flowId='112040'] 46 | 47 | ##teamcity[testIgnored name='test_incomplete' message='This test has not been implemented yet.' details=' C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php:44|n ' flowId='112040'] 48 | 49 | ##teamcity[testFinished name='test_incomplete' duration='0' flowId='112040'] 50 | 51 | ##teamcity[testStarted name='test_no_assertion' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\AssertionsTest.php::\Tests\AssertionsTest::test_no_assertion' flowId='112040'] 52 | 53 | ##teamcity[testFailed name='test_no_assertion' message='This test did not perform any assertions' details=' ' flowId='112040'] 54 | 55 | ##teamcity[testFinished name='test_no_assertion' duration='0' flowId='112040'] 56 | 57 | ##teamcity[testSuiteFinished name='Tests\AssertionsTest' flowId='112040'] 58 | 59 | ##teamcity[testSuiteStarted name='Tests\CalculatorTest' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php::\Tests\CalculatorTest' flowId='112040'] 60 | 61 | ##teamcity[testStarted name='test_sum' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php::\Tests\CalculatorTest::test_sum' flowId='112040'] 62 | 63 | ##teamcity[testFinished name='test_sum' duration='0' flowId='112040'] 64 | 65 | ##teamcity[testStarted name='test_sum_fail' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php::\Tests\CalculatorTest::test_sum_fail' flowId='112040'] 66 | 67 | ##teamcity[testFailed name='test_sum_fail' message='Failed asserting that 4 is identical to 3.' details=' C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php:29|n ' flowId='112040'] 68 | 69 | ##teamcity[testFinished name='test_sum_fail' duration='0' flowId='112040'] 70 | 71 | ##teamcity[testStarted name='test_sum_item' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php::\Tests\CalculatorTest::test_sum_item' flowId='112040'] 72 | 73 | ##teamcity[testFinished name='test_sum_item' duration='0' flowId='112040'] 74 | 75 | ##teamcity[testStarted name='test_sum_item_method_not_call' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php::\Tests\CalculatorTest::test_sum_item_method_not_call' flowId='112040'] 76 | 77 | ##teamcity[testFailed name='test_sum_item_method_not_call' message='Mockery\Exception\InvalidCountException : Method test() from Mockery_0_App_Item_App_Item should be called|r|n exactly 1 times but called 0 times.' details=' C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\CountValidator\Exact.php:38|n C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\Expectation.php:309|n C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\ExpectationDirector.php:119|n C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\Container.php:301|n C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery\Container.php:286|n C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\vendor\mockery\mockery\library\Mockery.php:165|n C:\Users\recca\Desktop\vscode-phpunit\server\tests\fixtures\project\tests\CalculatorTest.php:15|n ' flowId='112040'] 78 | 79 | ##teamcity[testFinished name='test_sum_item_method_not_call' duration='20' flowId='112040'] 80 | 81 | ##teamcity[testSuiteFinished name='Tests\CalculatorTest' flowId='112040'] 82 | 83 | ##teamcity[testSuiteFinished name='Package Test Suite' flowId='112040'] 84 | 85 | 86 | Time: 160 ms, Memory: 6.00MB 87 | 88 | 89 | ERRORS! 90 | Tests: 12, Assertions: 8, Errors: 1, Failures: 3, Skipped: 1, Incomplete: 1, Risky: 2. 91 | -------------------------------------------------------------------------------- /tests/fixtures/project/tests/AssertionsTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 12 | } 13 | 14 | public function test_error() 15 | { 16 | $this->assertTrue(false); 17 | } 18 | 19 | public function test_assertion_isnt_same() 20 | { 21 | $this->assertSame(['a' => 'b', 'c' => 'd'], ['e' => 'f', 'g', 'h']); 22 | } 23 | 24 | public function test_risky() 25 | { 26 | $a = 1; 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function it_should_be_annotation_test() 33 | { 34 | $this->assertTrue(true); 35 | } 36 | 37 | public function test_skipped() 38 | { 39 | $this->markTestSkipped('The MySQLi extension is not available.'); 40 | } 41 | 42 | public function test_incomplete() 43 | { 44 | $this->markTestIncomplete('This test has not been implemented yet.'); 45 | } 46 | 47 | public function test_no_assertion() 48 | { 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/fixtures/project/tests/CalculatorTest.php: -------------------------------------------------------------------------------- 1 | assertSame($calculator->sum(1, 2), 3); 23 | } 24 | 25 | public function test_sum_fail() 26 | { 27 | $calculator = new Calculator(); 28 | 29 | $this->assertSame($calculator->sum(1, 2), 4); 30 | } 31 | 32 | public function test_sum_item() 33 | { 34 | $calculator = new Calculator(); 35 | 36 | $a = new Item(1); 37 | $b = new Item(2); 38 | 39 | $this->assertSame($calculator->sumItem($a, $b), 3); 40 | } 41 | 42 | public function test_sum_item_method_not_call() 43 | { 44 | $calculator = new Calculator(); 45 | 46 | $a = m::mock(new Item(1)); 47 | $b = new Item(2); 48 | 49 | $a->shouldReceive('test')->once(); 50 | 51 | $this->assertSame($calculator->sumItem($a, $b), 3); 52 | } 53 | 54 | public function test_throw_exception() 55 | { 56 | $calculator = new Calculator(); 57 | $calculator->throwException(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/fixtures/project/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | /dev/null; cd "../phpunit/phpunit" && pwd) 4 | 5 | if [ -d /proc/cygdrive ] && [[ $(which php) == $(readlink -n /proc/cygdrive)/* ]]; then 6 | # We are in Cgywin using Windows php, so the path must be translated 7 | dir=$(cygpath -m "$dir"); 8 | fi 9 | 10 | "${dir}/phpunit" "$@" 11 | -------------------------------------------------------------------------------- /tests/fixtures/project/vendor-stub/bin/phpunit.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | setlocal DISABLEDELAYEDEXPANSION 3 | SET BIN_TARGET=%~dp0/../phpunit/phpunit/phpunit 4 | php "%BIN_TARGET%" %* 5 | -------------------------------------------------------------------------------- /tests/fixtures/project/vendor-stub/mockery/mockery/library/Mockery/Container.php: -------------------------------------------------------------------------------- 1 | _generator = $generator ?: \Mockery::getDefaultGenerator(); 77 | $this->_loader = $loader ?: \Mockery::getDefaultLoader(); 78 | } 79 | 80 | /** 81 | * Generates a new mock object for this container 82 | * 83 | * I apologies in advance for this. A God Method just fits the API which 84 | * doesn't require differentiating between classes, interfaces, abstracts, 85 | * names or partials - just so long as it's something that can be mocked. 86 | * I'll refactor it one day so it's easier to follow. 87 | * 88 | * @param array $args 89 | * 90 | * @return Mock 91 | * @throws Exception\RuntimeException 92 | */ 93 | public function mock(...$args) 94 | { 95 | $expectationClosure = null; 96 | $quickdefs = array(); 97 | $constructorArgs = null; 98 | $blocks = array(); 99 | 100 | if (count($args) > 1) { 101 | $finalArg = end($args); 102 | reset($args); 103 | if (is_callable($finalArg) && is_object($finalArg)) { 104 | $expectationClosure = array_pop($args); 105 | } 106 | } 107 | 108 | $builder = new MockConfigurationBuilder(); 109 | 110 | foreach ($args as $k => $arg) { 111 | if ($arg instanceof MockConfigurationBuilder) { 112 | $builder = $arg; 113 | unset($args[$k]); 114 | } 115 | } 116 | reset($args); 117 | 118 | $builder->setParameterOverrides(\Mockery::getConfiguration()->getInternalClassMethodParamMaps()); 119 | 120 | while (count($args) > 0) { 121 | $arg = current($args); 122 | // check for multiple interfaces 123 | if (is_string($arg) && strpos($arg, ',') && !strpos($arg, ']')) { 124 | $interfaces = explode(',', str_replace(' ', '', $arg)); 125 | $builder->addTargets($interfaces); 126 | array_shift($args); 127 | 128 | continue; 129 | } elseif (is_string($arg) && substr($arg, 0, 6) == 'alias:') { 130 | $name = array_shift($args); 131 | $name = str_replace('alias:', '', $name); 132 | $builder->addTarget('stdClass'); 133 | $builder->setName($name); 134 | continue; 135 | } elseif (is_string($arg) && substr($arg, 0, 9) == 'overload:') { 136 | $name = array_shift($args); 137 | $name = str_replace('overload:', '', $name); 138 | $builder->setInstanceMock(true); 139 | $builder->addTarget('stdClass'); 140 | $builder->setName($name); 141 | continue; 142 | } elseif (is_string($arg) && substr($arg, strlen($arg)-1, 1) == ']') { 143 | $parts = explode('[', $arg); 144 | if (!class_exists($parts[0], true) && !interface_exists($parts[0], true)) { 145 | throw new \Mockery\Exception('Can only create a partial mock from' 146 | . ' an existing class or interface'); 147 | } 148 | $class = $parts[0]; 149 | $parts[1] = str_replace(' ', '', $parts[1]); 150 | $partialMethods = explode(',', strtolower(rtrim($parts[1], ']'))); 151 | $builder->addTarget($class); 152 | $builder->setWhiteListedMethods($partialMethods); 153 | array_shift($args); 154 | continue; 155 | } elseif (is_string($arg) && (class_exists($arg, true) || interface_exists($arg, true) || trait_exists($arg, true))) { 156 | $class = array_shift($args); 157 | $builder->addTarget($class); 158 | continue; 159 | } elseif (is_string($arg) && !\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed() && (!class_exists($arg, true) && !interface_exists($arg, true))) { 160 | throw new \Mockery\Exception("Mockery can't find '$arg' so can't mock it"); 161 | } elseif (is_string($arg)) { 162 | if (!$this->isValidClassName($arg)) { 163 | throw new \Mockery\Exception('Class name contains invalid characters'); 164 | } 165 | $class = array_shift($args); 166 | $builder->addTarget($class); 167 | continue; 168 | } elseif (is_object($arg)) { 169 | $partial = array_shift($args); 170 | $builder->addTarget($partial); 171 | continue; 172 | } elseif (is_array($arg) && !empty($arg) && array_keys($arg) !== range(0, count($arg) - 1)) { 173 | // if associative array 174 | if (array_key_exists(self::BLOCKS, $arg)) { 175 | $blocks = $arg[self::BLOCKS]; 176 | } 177 | unset($arg[self::BLOCKS]); 178 | $quickdefs = array_shift($args); 179 | continue; 180 | } elseif (is_array($arg)) { 181 | $constructorArgs = array_shift($args); 182 | continue; 183 | } 184 | 185 | throw new \Mockery\Exception( 186 | 'Unable to parse arguments sent to ' 187 | . get_class($this) . '::mock()' 188 | ); 189 | } 190 | 191 | $builder->addBlackListedMethods($blocks); 192 | 193 | if (defined('HHVM_VERSION') 194 | && isset($class) 195 | && ($class === 'Exception' || is_subclass_of($class, 'Exception'))) { 196 | $builder->addBlackListedMethod("setTraceOptions"); 197 | $builder->addBlackListedMethod("getTraceOptions"); 198 | } 199 | 200 | if (!is_null($constructorArgs)) { 201 | $builder->addBlackListedMethod("__construct"); // we need to pass through 202 | } else { 203 | $builder->setMockOriginalDestructor(true); 204 | } 205 | 206 | if (!empty($partialMethods) && $constructorArgs === null) { 207 | $constructorArgs = array(); 208 | } 209 | 210 | $config = $builder->getMockConfiguration(); 211 | 212 | $this->checkForNamedMockClashes($config); 213 | 214 | $def = $this->getGenerator()->generate($config); 215 | 216 | if (class_exists($def->getClassName(), $attemptAutoload = false)) { 217 | $rfc = new \ReflectionClass($def->getClassName()); 218 | if (!$rfc->implementsInterface("Mockery\MockInterface")) { 219 | throw new \Mockery\Exception\RuntimeException("Could not load mock {$def->getClassName()}, class already exists"); 220 | } 221 | } 222 | 223 | $this->getLoader()->load($def); 224 | 225 | $mock = $this->_getInstance($def->getClassName(), $constructorArgs); 226 | $mock->mockery_init($this, $config->getTargetObject()); 227 | 228 | if (!empty($quickdefs)) { 229 | $mock->shouldReceive($quickdefs)->byDefault(); 230 | } 231 | if (!empty($expectationClosure)) { 232 | $expectationClosure($mock); 233 | } 234 | $this->rememberMock($mock); 235 | return $mock; 236 | } 237 | 238 | public function instanceMock() 239 | { 240 | } 241 | 242 | public function getLoader() 243 | { 244 | return $this->_loader; 245 | } 246 | 247 | public function getGenerator() 248 | { 249 | return $this->_generator; 250 | } 251 | 252 | /** 253 | * @param string $method 254 | * @return string|null 255 | */ 256 | public function getKeyOfDemeterMockFor($method) 257 | { 258 | $keys = array_keys($this->_mocks); 259 | $match = preg_grep("/__demeter_{$method}$/", $keys); 260 | if (count($match) == 1) { 261 | $res = array_values($match); 262 | if (count($res) > 0) { 263 | return $res[0]; 264 | } 265 | } 266 | return null; 267 | } 268 | 269 | /** 270 | * @return array 271 | */ 272 | public function getMocks() 273 | { 274 | return $this->_mocks; 275 | } 276 | 277 | /** 278 | * Tear down tasks for this container 279 | * 280 | * @throws \Exception 281 | * @return void 282 | */ 283 | public function mockery_teardown() 284 | { 285 | try { 286 | $this->mockery_verify(); 287 | } catch (\Exception $e) { 288 | $this->mockery_close(); 289 | throw $e; 290 | } 291 | } 292 | 293 | /** 294 | * Verify the container mocks 295 | * 296 | * @return void 297 | */ 298 | public function mockery_verify() 299 | { 300 | foreach ($this->_mocks as $mock) { 301 | $mock->mockery_verify(); 302 | } 303 | } 304 | 305 | /** 306 | * Retrieves all exceptions thrown by mocks 307 | * 308 | * @return array 309 | */ 310 | public function mockery_thrownExceptions() 311 | { 312 | $e = []; 313 | 314 | foreach ($this->_mocks as $mock) { 315 | $e = array_merge($e, $mock->mockery_thrownExceptions()); 316 | } 317 | 318 | return $e; 319 | } 320 | 321 | /** 322 | * Reset the container to its original state 323 | * 324 | * @return void 325 | */ 326 | public function mockery_close() 327 | { 328 | foreach ($this->_mocks as $mock) { 329 | $mock->mockery_teardown(); 330 | } 331 | $this->_mocks = array(); 332 | } 333 | 334 | /** 335 | * Fetch the next available allocation order number 336 | * 337 | * @return int 338 | */ 339 | public function mockery_allocateOrder() 340 | { 341 | $this->_allocatedOrder += 1; 342 | return $this->_allocatedOrder; 343 | } 344 | 345 | /** 346 | * Set ordering for a group 347 | * 348 | * @param mixed $group 349 | * @param int $order 350 | */ 351 | public function mockery_setGroup($group, $order) 352 | { 353 | $this->_groups[$group] = $order; 354 | } 355 | 356 | /** 357 | * Fetch array of ordered groups 358 | * 359 | * @return array 360 | */ 361 | public function mockery_getGroups() 362 | { 363 | return $this->_groups; 364 | } 365 | 366 | /** 367 | * Set current ordered number 368 | * 369 | * @param int $order 370 | * @return int The current order number that was set 371 | */ 372 | public function mockery_setCurrentOrder($order) 373 | { 374 | $this->_currentOrder = $order; 375 | return $this->_currentOrder; 376 | } 377 | 378 | /** 379 | * Get current ordered number 380 | * 381 | * @return int 382 | */ 383 | public function mockery_getCurrentOrder() 384 | { 385 | return $this->_currentOrder; 386 | } 387 | 388 | /** 389 | * Validate the current mock's ordering 390 | * 391 | * @param string $method 392 | * @param int $order 393 | * @throws \Mockery\Exception 394 | * @return void 395 | */ 396 | public function mockery_validateOrder($method, $order, \Mockery\MockInterface $mock) 397 | { 398 | if ($order < $this->_currentOrder) { 399 | $exception = new \Mockery\Exception\InvalidOrderException( 400 | 'Method ' . $method . ' called out of order: expected order ' 401 | . $order . ', was ' . $this->_currentOrder 402 | ); 403 | $exception->setMock($mock) 404 | ->setMethodName($method) 405 | ->setExpectedOrder($order) 406 | ->setActualOrder($this->_currentOrder); 407 | throw $exception; 408 | } 409 | $this->mockery_setCurrentOrder($order); 410 | } 411 | 412 | /** 413 | * Gets the count of expectations on the mocks 414 | * 415 | * @return int 416 | */ 417 | public function mockery_getExpectationCount() 418 | { 419 | $count = 0; 420 | foreach ($this->_mocks as $mock) { 421 | $count += $mock->mockery_getExpectationCount(); 422 | } 423 | return $count; 424 | } 425 | 426 | /** 427 | * Store a mock and set its container reference 428 | * 429 | * @param \Mockery\Mock 430 | * @return \Mockery\MockInterface 431 | */ 432 | public function rememberMock(\Mockery\MockInterface $mock) 433 | { 434 | if (!isset($this->_mocks[get_class($mock)])) { 435 | $this->_mocks[get_class($mock)] = $mock; 436 | } else { 437 | /** 438 | * This condition triggers for an instance mock where origin mock 439 | * is already remembered 440 | */ 441 | $this->_mocks[] = $mock; 442 | } 443 | return $mock; 444 | } 445 | 446 | /** 447 | * Retrieve the last remembered mock object, which is the same as saying 448 | * retrieve the current mock being programmed where you have yet to call 449 | * mock() to change it - thus why the method name is "self" since it will be 450 | * be used during the programming of the same mock. 451 | * 452 | * @return \Mockery\Mock 453 | */ 454 | public function self() 455 | { 456 | $mocks = array_values($this->_mocks); 457 | $index = count($mocks) - 1; 458 | return $mocks[$index]; 459 | } 460 | 461 | /** 462 | * Return a specific remembered mock according to the array index it 463 | * was stored to in this container instance 464 | * 465 | * @return \Mockery\Mock 466 | */ 467 | public function fetchMock($reference) 468 | { 469 | if (isset($this->_mocks[$reference])) { 470 | return $this->_mocks[$reference]; 471 | } 472 | } 473 | 474 | protected function _getInstance($mockName, $constructorArgs = null) 475 | { 476 | if ($constructorArgs !== null) { 477 | $r = new \ReflectionClass($mockName); 478 | return $r->newInstanceArgs($constructorArgs); 479 | } 480 | 481 | try { 482 | $instantiator = new Instantiator; 483 | $instance = $instantiator->instantiate($mockName); 484 | } catch (\Exception $ex) { 485 | $internalMockName = $mockName . '_Internal'; 486 | 487 | if (!class_exists($internalMockName)) { 488 | eval("class $internalMockName extends $mockName {" . 489 | 'public function __construct() {}' . 490 | '}'); 491 | } 492 | 493 | $instance = new $internalMockName(); 494 | } 495 | 496 | return $instance; 497 | } 498 | 499 | protected function checkForNamedMockClashes($config) 500 | { 501 | $name = $config->getName(); 502 | 503 | if (!$name) { 504 | return; 505 | } 506 | 507 | $hash = $config->getHash(); 508 | 509 | if (isset($this->_namedMocks[$name])) { 510 | if ($hash !== $this->_namedMocks[$name]) { 511 | throw new \Mockery\Exception( 512 | "The mock named '$name' has been already defined with a different mock configuration" 513 | ); 514 | } 515 | } 516 | 517 | $this->_namedMocks[$name] = $hash; 518 | } 519 | 520 | /** 521 | * see http://php.net/manual/en/language.oop5.basic.php 522 | * @param string $className 523 | * @return bool 524 | */ 525 | public function isValidClassName($className) 526 | { 527 | $pos = strpos($className, '\\'); 528 | if ($pos === 0) { 529 | $className = substr($className, 1); // remove the first backslash 530 | } 531 | // all the namespaces and class name should match the regex 532 | $invalidNames = array_filter(explode('\\', $className), function ($name) { 533 | return !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $name); 534 | }); 535 | return empty($invalidNames); 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /tests/fixtures/project/vendor-stub/mockery/mockery/library/Mockery/CountValidator/Exact.php: -------------------------------------------------------------------------------- 1 | _limit !== $n) { 36 | $because = $this->_expectation->getExceptionMessage(); 37 | 38 | $exception = new Mockery\Exception\InvalidCountException( 39 | 'Method ' . (string) $this->_expectation 40 | . ' from ' . $this->_expectation->getMock()->mockery_getName() 41 | . ' should be called' . PHP_EOL 42 | . ' exactly ' . $this->_limit . ' times but called ' . $n 43 | . ' times.' 44 | . ($because ? ' Because ' . $this->_expectation->getExceptionMessage() : '') 45 | ); 46 | $exception->setMock($this->_expectation->getMock()) 47 | ->setMethodName((string) $this->_expectation) 48 | ->setExpectedCountComparative('=') 49 | ->setExpectedCount($this->_limit) 50 | ->setActualCount($n); 51 | throw $exception; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/fixtures/project/vendor-stub/mockery/mockery/library/Mockery/Exception.php: -------------------------------------------------------------------------------- 1 | _mock = $mock; 155 | $this->_name = $name; 156 | $this->withAnyArgs(); 157 | } 158 | 159 | /** 160 | * Return a string with the method name and arguments formatted 161 | * 162 | * @param string $name Name of the expected method 163 | * @param array $args List of arguments to the method 164 | * @return string 165 | */ 166 | public function __toString() 167 | { 168 | return \Mockery::formatArgs($this->_name, $this->_expectedArgs); 169 | } 170 | 171 | /** 172 | * Verify the current call, i.e. that the given arguments match those 173 | * of this expectation 174 | * 175 | * @param array $args 176 | * @return mixed 177 | */ 178 | public function verifyCall(array $args) 179 | { 180 | $this->validateOrder(); 181 | $this->_actualCount++; 182 | if (true === $this->_passthru) { 183 | return $this->_mock->mockery_callSubjectMethod($this->_name, $args); 184 | } 185 | 186 | $return = $this->_getReturnValue($args); 187 | $this->throwAsNecessary($return); 188 | $this->_setValues(); 189 | 190 | return $return; 191 | } 192 | 193 | /** 194 | * Throws an exception if the expectation has been configured to do so 195 | * 196 | * @throws \Exception|\Throwable 197 | * @return void 198 | */ 199 | private function throwAsNecessary($return) 200 | { 201 | if (!$this->_throw) { 202 | return; 203 | } 204 | 205 | $type = version_compare(PHP_VERSION, '7.0.0') >= 0 206 | ? "\Throwable" 207 | : "\Exception"; 208 | 209 | if ($return instanceof $type) { 210 | throw $return; 211 | } 212 | 213 | return; 214 | } 215 | 216 | /** 217 | * Sets public properties with queued values to the mock object 218 | * 219 | * @param array $args 220 | * @return mixed 221 | */ 222 | protected function _setValues() 223 | { 224 | $mockClass = get_class($this->_mock); 225 | $container = $this->_mock->mockery_getContainer(); 226 | $mocks = $container->getMocks(); 227 | foreach ($this->_setQueue as $name => &$values) { 228 | if (count($values) > 0) { 229 | $value = array_shift($values); 230 | foreach ($mocks as $mock) { 231 | if (is_a($mock, $mockClass)) { 232 | $mock->{$name} = $value; 233 | } 234 | } 235 | } 236 | } 237 | } 238 | 239 | /** 240 | * Fetch the return value for the matching args 241 | * 242 | * @param array $args 243 | * @return mixed 244 | */ 245 | protected function _getReturnValue(array $args) 246 | { 247 | if (count($this->_closureQueue) > 1) { 248 | return call_user_func_array(array_shift($this->_closureQueue), $args); 249 | } elseif (count($this->_closureQueue) > 0) { 250 | return call_user_func_array(current($this->_closureQueue), $args); 251 | } elseif (count($this->_returnQueue) > 1) { 252 | return array_shift($this->_returnQueue); 253 | } elseif (count($this->_returnQueue) > 0) { 254 | return current($this->_returnQueue); 255 | } 256 | 257 | return $this->_mock->mockery_returnValueForMethod($this->_name); 258 | } 259 | 260 | /** 261 | * Checks if this expectation is eligible for additional calls 262 | * 263 | * @return bool 264 | */ 265 | public function isEligible() 266 | { 267 | foreach ($this->_countValidators as $validator) { 268 | if (!$validator->isEligible($this->_actualCount)) { 269 | return false; 270 | } 271 | } 272 | return true; 273 | } 274 | 275 | /** 276 | * Check if there is a constraint on call count 277 | * 278 | * @return bool 279 | */ 280 | public function isCallCountConstrained() 281 | { 282 | return (count($this->_countValidators) > 0); 283 | } 284 | 285 | /** 286 | * Verify call order 287 | * 288 | * @return void 289 | */ 290 | public function validateOrder() 291 | { 292 | if ($this->_orderNumber) { 293 | $this->_mock->mockery_validateOrder((string) $this, $this->_orderNumber, $this->_mock); 294 | } 295 | if ($this->_globalOrderNumber) { 296 | $this->_mock->mockery_getContainer() 297 | ->mockery_validateOrder((string) $this, $this->_globalOrderNumber, $this->_mock); 298 | } 299 | } 300 | 301 | /** 302 | * Verify this expectation 303 | * 304 | * @return bool 305 | */ 306 | public function verify() 307 | { 308 | foreach ($this->_countValidators as $validator) { 309 | $validator->validate($this->_actualCount); 310 | } 311 | } 312 | 313 | /** 314 | * Check if the registered expectation is an ArgumentListMatcher 315 | * @return bool 316 | */ 317 | private function isArgumentListMatcher() 318 | { 319 | return (count($this->_expectedArgs) === 1 && ($this->_expectedArgs[0] instanceof ArgumentListMatcher)); 320 | } 321 | 322 | /** 323 | * Check if passed arguments match an argument expectation 324 | * 325 | * @param array $args 326 | * @return bool 327 | */ 328 | public function matchArgs(array $args) 329 | { 330 | if ($this->isArgumentListMatcher()) { 331 | return $this->_matchArg($this->_expectedArgs[0], $args); 332 | } 333 | $argCount = count($args); 334 | if ($argCount !== count((array) $this->_expectedArgs)) { 335 | return false; 336 | } 337 | for ($i=0; $i<$argCount; $i++) { 338 | $param =& $args[$i]; 339 | if (!$this->_matchArg($this->_expectedArgs[$i], $param)) { 340 | return false; 341 | } 342 | } 343 | 344 | return true; 345 | } 346 | 347 | /** 348 | * Check if passed argument matches an argument expectation 349 | * 350 | * @param mixed $expected 351 | * @param mixed &$actual 352 | * @return bool 353 | */ 354 | protected function _matchArg($expected, &$actual) 355 | { 356 | if ($expected === $actual) { 357 | return true; 358 | } 359 | if (!is_object($expected) && !is_object($actual) && $expected == $actual) { 360 | return true; 361 | } 362 | if (is_string($expected) && is_object($actual)) { 363 | $result = $actual instanceof $expected; 364 | if ($result) { 365 | return true; 366 | } 367 | } 368 | if ($expected instanceof \Mockery\Matcher\MatcherAbstract) { 369 | return $expected->match($actual); 370 | } 371 | if ($expected instanceof \Hamcrest\Matcher || $expected instanceof \Hamcrest_Matcher) { 372 | return $expected->matches($actual); 373 | } 374 | return false; 375 | } 376 | 377 | /** 378 | * Expected argument setter for the expectation 379 | * 380 | * @param mixed[] ... 381 | * @return self 382 | */ 383 | public function with(...$args) 384 | { 385 | return $this->withArgs($args); 386 | } 387 | 388 | /** 389 | * Expected arguments for the expectation passed as an array 390 | * 391 | * @param array $arguments 392 | * @return self 393 | */ 394 | private function withArgsInArray(array $arguments) 395 | { 396 | if (empty($arguments)) { 397 | return $this->withNoArgs(); 398 | } 399 | $this->_expectedArgs = $arguments; 400 | return $this; 401 | } 402 | 403 | /** 404 | * Expected arguments have to be matched by the given closure. 405 | * 406 | * @param Closure $closure 407 | * @return self 408 | */ 409 | private function withArgsMatchedByClosure(Closure $closure) 410 | { 411 | $this->_expectedArgs = [new MultiArgumentClosure($closure)]; 412 | return $this; 413 | } 414 | 415 | /** 416 | * Expected arguments for the expectation passed as an array or a closure that matches each passed argument on 417 | * each function call. 418 | * 419 | * @param array|Closure $argsOrClosure 420 | * @return self 421 | */ 422 | public function withArgs($argsOrClosure) 423 | { 424 | if (is_array($argsOrClosure)) { 425 | $this->withArgsInArray($argsOrClosure); 426 | } elseif ($argsOrClosure instanceof Closure) { 427 | $this->withArgsMatchedByClosure($argsOrClosure); 428 | } else { 429 | throw new \InvalidArgumentException(sprintf('Call to %s with an invalid argument (%s), only array and '. 430 | 'closure are allowed', __METHOD__, $argsOrClosure)); 431 | } 432 | return $this; 433 | } 434 | 435 | /** 436 | * Set with() as no arguments expected 437 | * 438 | * @return self 439 | */ 440 | public function withNoArgs() 441 | { 442 | $this->_expectedArgs = [new NoArgs()]; 443 | return $this; 444 | } 445 | 446 | /** 447 | * Set expectation that any arguments are acceptable 448 | * 449 | * @return self 450 | */ 451 | public function withAnyArgs() 452 | { 453 | $this->_expectedArgs = [new AnyArgs()]; 454 | return $this; 455 | } 456 | 457 | /** 458 | * Set a return value, or sequential queue of return values 459 | * 460 | * @param mixed[] ... 461 | * @return self 462 | */ 463 | public function andReturn(...$args) 464 | { 465 | $this->_returnQueue = $args; 466 | return $this; 467 | } 468 | 469 | /** 470 | * Set a return value, or sequential queue of return values 471 | * 472 | * @param mixed[] ... 473 | * @return self 474 | */ 475 | public function andReturns(...$args) 476 | { 477 | return call_user_func_array([$this, 'andReturn'], $args); 478 | } 479 | 480 | /** 481 | * Return this mock, like a fluent interface 482 | * 483 | * @return self 484 | */ 485 | public function andReturnSelf() 486 | { 487 | return $this->andReturn($this->_mock); 488 | } 489 | 490 | /** 491 | * Set a sequential queue of return values with an array 492 | * 493 | * @param array $values 494 | * @return self 495 | */ 496 | public function andReturnValues(array $values) 497 | { 498 | call_user_func_array(array($this, 'andReturn'), $values); 499 | return $this; 500 | } 501 | 502 | /** 503 | * Set a closure or sequence of closures with which to generate return 504 | * values. The arguments passed to the expected method are passed to the 505 | * closures as parameters. 506 | * 507 | * @param callable[] $args 508 | * @return self 509 | */ 510 | public function andReturnUsing(...$args) 511 | { 512 | $this->_closureQueue = $args; 513 | return $this; 514 | } 515 | 516 | /** 517 | * Return a self-returning black hole object. 518 | * 519 | * @return self 520 | */ 521 | public function andReturnUndefined() 522 | { 523 | $this->andReturn(new \Mockery\Undefined); 524 | return $this; 525 | } 526 | 527 | /** 528 | * Return null. This is merely a language construct for Mock describing. 529 | * 530 | * @return self 531 | */ 532 | public function andReturnNull() 533 | { 534 | return $this->andReturn(null); 535 | } 536 | 537 | public function andReturnFalse() 538 | { 539 | return $this->andReturn(false); 540 | } 541 | 542 | public function andReturnTrue() 543 | { 544 | return $this->andReturn(true); 545 | } 546 | 547 | /** 548 | * Set Exception class and arguments to that class to be thrown 549 | * 550 | * @param string|\Exception $exception 551 | * @param string $message 552 | * @param int $code 553 | * @param \Exception $previous 554 | * @return self 555 | */ 556 | public function andThrow($exception, $message = '', $code = 0, \Exception $previous = null) 557 | { 558 | $this->_throw = true; 559 | if (is_object($exception)) { 560 | $this->andReturn($exception); 561 | } else { 562 | $this->andReturn(new $exception($message, $code, $previous)); 563 | } 564 | return $this; 565 | } 566 | 567 | public function andThrows($exception, $message = '', $code = 0, \Exception $previous = null) 568 | { 569 | return $this->andThrow($exception, $message, $code, $previous); 570 | } 571 | 572 | /** 573 | * Set Exception classes to be thrown 574 | * 575 | * @param array $exceptions 576 | * @return self 577 | */ 578 | public function andThrowExceptions(array $exceptions) 579 | { 580 | $this->_throw = true; 581 | foreach ($exceptions as $exception) { 582 | if (!is_object($exception)) { 583 | throw new Exception('You must pass an array of exception objects to andThrowExceptions'); 584 | } 585 | } 586 | return $this->andReturnValues($exceptions); 587 | } 588 | 589 | /** 590 | * Register values to be set to a public property each time this expectation occurs 591 | * 592 | * @param string $name 593 | * @param array $values 594 | * @return self 595 | */ 596 | public function andSet($name, ...$values) 597 | { 598 | $this->_setQueue[$name] = $values; 599 | return $this; 600 | } 601 | 602 | /** 603 | * Alias to andSet(). Allows the natural English construct 604 | * - set('foo', 'bar')->andReturn('bar') 605 | * 606 | * @param string $name 607 | * @param mixed $value 608 | * @return self 609 | */ 610 | public function set($name, $value) 611 | { 612 | return call_user_func_array(array($this, 'andSet'), func_get_args()); 613 | } 614 | 615 | /** 616 | * Indicates this expectation should occur zero or more times 617 | * 618 | * @return self 619 | */ 620 | public function zeroOrMoreTimes() 621 | { 622 | $this->atLeast()->never(); 623 | } 624 | 625 | /** 626 | * Indicates the number of times this expectation should occur 627 | * 628 | * @param int $limit 629 | * @throws \InvalidArgumentException 630 | * @return self 631 | */ 632 | public function times($limit = null) 633 | { 634 | if (is_null($limit)) { 635 | return $this; 636 | } 637 | if (!is_int($limit)) { 638 | throw new \InvalidArgumentException('The passed Times limit should be an integer value'); 639 | } 640 | $this->_countValidators[$this->_countValidatorClass] = new $this->_countValidatorClass($this, $limit); 641 | $this->_countValidatorClass = 'Mockery\CountValidator\Exact'; 642 | return $this; 643 | } 644 | 645 | /** 646 | * Indicates that this expectation is never expected to be called 647 | * 648 | * @return self 649 | */ 650 | public function never() 651 | { 652 | return $this->times(0); 653 | } 654 | 655 | /** 656 | * Indicates that this expectation is expected exactly once 657 | * 658 | * @return self 659 | */ 660 | public function once() 661 | { 662 | return $this->times(1); 663 | } 664 | 665 | /** 666 | * Indicates that this expectation is expected exactly twice 667 | * 668 | * @return self 669 | */ 670 | public function twice() 671 | { 672 | return $this->times(2); 673 | } 674 | 675 | /** 676 | * Sets next count validator to the AtLeast instance 677 | * 678 | * @return self 679 | */ 680 | public function atLeast() 681 | { 682 | $this->_countValidatorClass = 'Mockery\CountValidator\AtLeast'; 683 | return $this; 684 | } 685 | 686 | /** 687 | * Sets next count validator to the AtMost instance 688 | * 689 | * @return self 690 | */ 691 | public function atMost() 692 | { 693 | $this->_countValidatorClass = 'Mockery\CountValidator\AtMost'; 694 | return $this; 695 | } 696 | 697 | /** 698 | * Shorthand for setting minimum and maximum constraints on call counts 699 | * 700 | * @param int $minimum 701 | * @param int $maximum 702 | */ 703 | public function between($minimum, $maximum) 704 | { 705 | return $this->atLeast()->times($minimum)->atMost()->times($maximum); 706 | } 707 | 708 | 709 | /** 710 | * Set the exception message 711 | * 712 | * @param string $message 713 | * @return $this 714 | */ 715 | public function because($message) 716 | { 717 | $this->_because = $message; 718 | return $this; 719 | } 720 | 721 | /** 722 | * Indicates that this expectation must be called in a specific given order 723 | * 724 | * @param string $group Name of the ordered group 725 | * @return self 726 | */ 727 | public function ordered($group = null) 728 | { 729 | if ($this->_globally) { 730 | $this->_globalOrderNumber = $this->_defineOrdered($group, $this->_mock->mockery_getContainer()); 731 | } else { 732 | $this->_orderNumber = $this->_defineOrdered($group, $this->_mock); 733 | } 734 | $this->_globally = false; 735 | return $this; 736 | } 737 | 738 | /** 739 | * Indicates call order should apply globally 740 | * 741 | * @return self 742 | */ 743 | public function globally() 744 | { 745 | $this->_globally = true; 746 | return $this; 747 | } 748 | 749 | /** 750 | * Setup the ordering tracking on the mock or mock container 751 | * 752 | * @param string $group 753 | * @param object $ordering 754 | * @return int 755 | */ 756 | protected function _defineOrdered($group, $ordering) 757 | { 758 | $groups = $ordering->mockery_getGroups(); 759 | if (is_null($group)) { 760 | $result = $ordering->mockery_allocateOrder(); 761 | } elseif (isset($groups[$group])) { 762 | $result = $groups[$group]; 763 | } else { 764 | $result = $ordering->mockery_allocateOrder(); 765 | $ordering->mockery_setGroup($group, $result); 766 | } 767 | return $result; 768 | } 769 | 770 | /** 771 | * Return order number 772 | * 773 | * @return int 774 | */ 775 | public function getOrderNumber() 776 | { 777 | return $this->_orderNumber; 778 | } 779 | 780 | /** 781 | * Mark this expectation as being a default 782 | * 783 | * @return self 784 | */ 785 | public function byDefault() 786 | { 787 | $director = $this->_mock->mockery_getExpectationsFor($this->_name); 788 | if (!empty($director)) { 789 | $director->makeExpectationDefault($this); 790 | } 791 | return $this; 792 | } 793 | 794 | /** 795 | * Return the parent mock of the expectation 796 | * 797 | * @return \Mockery\MockInterface 798 | */ 799 | public function getMock() 800 | { 801 | return $this->_mock; 802 | } 803 | 804 | /** 805 | * Flag this expectation as calling the original class method with the 806 | * any provided arguments instead of using a return value queue. 807 | * 808 | * @return self 809 | */ 810 | public function passthru() 811 | { 812 | if ($this->_mock instanceof Mock) { 813 | throw new Exception( 814 | 'Mock Objects not created from a loaded/existing class are ' 815 | . 'incapable of passing method calls through to a parent class' 816 | ); 817 | } 818 | $this->_passthru = true; 819 | return $this; 820 | } 821 | 822 | /** 823 | * Cloning logic 824 | * 825 | */ 826 | public function __clone() 827 | { 828 | $newValidators = array(); 829 | $countValidators = $this->_countValidators; 830 | foreach ($countValidators as $validator) { 831 | $newValidators[] = clone $validator; 832 | } 833 | $this->_countValidators = $newValidators; 834 | } 835 | 836 | public function getName() 837 | { 838 | return $this->_name; 839 | } 840 | 841 | public function getExceptionMessage() 842 | { 843 | return $this->_because; 844 | } 845 | } 846 | -------------------------------------------------------------------------------- /tests/fixtures/project/vendor-stub/mockery/mockery/library/Mockery/ExpectationDirector.php: -------------------------------------------------------------------------------- 1 | _name = $name; 69 | $this->_mock = $mock; 70 | } 71 | 72 | /** 73 | * Add a new expectation to the director 74 | * 75 | * @param \Mockery\Expectation $expectation 76 | */ 77 | public function addExpectation(\Mockery\Expectation $expectation) 78 | { 79 | $this->_expectations[] = $expectation; 80 | } 81 | 82 | /** 83 | * Handle a method call being directed by this instance 84 | * 85 | * @param array $args 86 | * @return mixed 87 | */ 88 | public function call(array $args) 89 | { 90 | $expectation = $this->findExpectation($args); 91 | if (is_null($expectation)) { 92 | $exception = new \Mockery\Exception\NoMatchingExpectationException( 93 | 'No matching handler found for ' 94 | . $this->_mock->mockery_getName() . '::' 95 | . \Mockery::formatArgs($this->_name, $args) 96 | . '. Either the method was unexpected or its arguments matched' 97 | . ' no expected argument list for this method' 98 | . PHP_EOL . PHP_EOL 99 | . \Mockery::formatObjects($args) 100 | ); 101 | $exception->setMock($this->_mock) 102 | ->setMethodName($this->_name) 103 | ->setActualArguments($args); 104 | throw $exception; 105 | } 106 | return $expectation->verifyCall($args); 107 | } 108 | 109 | /** 110 | * Verify all expectations of the director 111 | * 112 | * @throws \Mockery\CountValidator\Exception 113 | * @return void 114 | */ 115 | public function verify() 116 | { 117 | if (!empty($this->_expectations)) { 118 | foreach ($this->_expectations as $exp) { 119 | $exp->verify(); 120 | } 121 | } else { 122 | foreach ($this->_defaults as $exp) { 123 | $exp->verify(); 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * Attempt to locate an expectation matching the provided args 130 | * 131 | * @param array $args 132 | * @return mixed 133 | */ 134 | public function findExpectation(array $args) 135 | { 136 | $expectation = null; 137 | 138 | if (!empty($this->_expectations)) { 139 | $expectation = $this->_findExpectationIn($this->_expectations, $args); 140 | } 141 | 142 | if ($expectation === null && !empty($this->_defaults)) { 143 | $expectation = $this->_findExpectationIn($this->_defaults, $args); 144 | } 145 | 146 | return $expectation; 147 | } 148 | 149 | /** 150 | * Make the given expectation a default for all others assuming it was 151 | * correctly created last 152 | * 153 | * @param \Mockery\Expectation 154 | */ 155 | public function makeExpectationDefault(\Mockery\Expectation $expectation) 156 | { 157 | $last = end($this->_expectations); 158 | if ($last === $expectation) { 159 | array_pop($this->_expectations); 160 | array_unshift($this->_defaults, $expectation); 161 | } else { 162 | throw new \Mockery\Exception( 163 | 'Cannot turn a previously defined expectation into a default' 164 | ); 165 | } 166 | } 167 | 168 | /** 169 | * Search current array of expectations for a match 170 | * 171 | * @param array $expectations 172 | * @param array $args 173 | * @return mixed 174 | */ 175 | protected function _findExpectationIn(array $expectations, array $args) 176 | { 177 | foreach ($expectations as $exp) { 178 | if ($exp->isEligible() && $exp->matchArgs($args)) { 179 | return $exp; 180 | } 181 | } 182 | foreach ($expectations as $exp) { 183 | if ($exp->matchArgs($args)) { 184 | return $exp; 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * Return all expectations assigned to this director 191 | * 192 | * @return array 193 | */ 194 | public function getExpectations() 195 | { 196 | return $this->_expectations; 197 | } 198 | 199 | /** 200 | * Return all expectations assigned to this director 201 | * 202 | * @return array 203 | */ 204 | public function getDefaultExpectations() 205 | { 206 | return $this->_defaults; 207 | } 208 | 209 | /** 210 | * Return the number of expectations assigned to this director. 211 | * 212 | * @return int 213 | */ 214 | public function getExpectationCount() 215 | { 216 | return count($this->getExpectations()); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tests/fixtures/teamcity.txt: -------------------------------------------------------------------------------- 1 | PHPUnit 6.4.2 by Sebastian Bergmann and contributors. 2 | 3 | 4 | ##teamcity[testCount count='11' flowId='11960'] 5 | 6 | ##teamcity[testSuiteStarted name='tests/fixtures/' flowId='11960'] 7 | 8 | ##teamcity[testSuiteStarted name='PHPUnit2Test' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php::\PHPUnit2Test' flowId='11960'] 9 | 10 | ##teamcity[testStarted name='testPassed' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php::\PHPUnit2Test::testPassed' flowId='11960'] 11 | 12 | ##teamcity[testFinished name='testPassed' duration='20' flowId='11960'] 13 | 14 | ##teamcity[testStarted name='testFailed' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php::\PHPUnit2Test::testFailed' flowId='11960'] 15 | 16 | ##teamcity[testFailed name='testFailed' message='Failed asserting that false is true.' details=' C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php:20|n ' flowId='11960'] 17 | 18 | ##teamcity[testFinished name='testFailed' duration='0' flowId='11960'] 19 | 20 | ##teamcity[testStarted name='testSkipped' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php::\PHPUnit2Test::testSkipped' flowId='11960'] 21 | 22 | ##teamcity[testIgnored name='testSkipped' message='The MySQLi extension is not available.' details=' C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php:25|n ' flowId='11960'] 23 | 24 | ##teamcity[testFinished name='testSkipped' duration='0' flowId='11960'] 25 | 26 | ##teamcity[testStarted name='testIncomplete' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php::\PHPUnit2Test::testIncomplete' flowId='11960'] 27 | 28 | ##teamcity[testIgnored name='testIncomplete' message='This test has not been implemented yet.' details=' C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php:30|n ' flowId='11960'] 29 | 30 | ##teamcity[testFinished name='testIncomplete' duration='0' flowId='11960'] 31 | 32 | ##teamcity[testStarted name='testNoAssertions' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnit2Test.php::\PHPUnit2Test::testNoAssertions' flowId='11960'] 33 | 34 | ##teamcity[testFailed name='testNoAssertions' message='This test did not perform any assertions' details=' ' flowId='11960'] 35 | 36 | ##teamcity[testFinished name='testNoAssertions' duration='0' flowId='11960'] 37 | 38 | ##teamcity[testSuiteFinished name='PHPUnit2Test' flowId='11960'] 39 | 40 | ##teamcity[testSuiteStarted name='PHPUnitTest' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::\PHPUnitTest' flowId='11960'] 41 | 42 | ##teamcity[testStarted name='testPassed' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::\PHPUnitTest::testPassed' flowId='11960'] 43 | 44 | ##teamcity[testFinished name='testPassed' duration='0' flowId='11960'] 45 | 46 | ##teamcity[testStarted name='testFailed' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::\PHPUnitTest::testFailed' flowId='11960'] 47 | 48 | ##teamcity[testFailed name='testFailed' message='Failed asserting that false is true.' details=' C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php:20|n ' flowId='11960'] 49 | 50 | ##teamcity[testFinished name='testFailed' duration='0' flowId='11960'] 51 | 52 | ##teamcity[testStarted name='testSkipped' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::\PHPUnitTest::testSkipped' flowId='11960'] 53 | 54 | ##teamcity[testIgnored name='testSkipped' message='The MySQLi extension is not available.' details=' C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php:25|n ' flowId='11960'] 55 | 56 | ##teamcity[testFinished name='testSkipped' duration='0' flowId='11960'] 57 | 58 | ##teamcity[testStarted name='testIncomplete' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::\PHPUnitTest::testIncomplete' flowId='11960'] 59 | 60 | ##teamcity[testIgnored name='testIncomplete' message='This test has not been implemented yet.' details=' C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php:30|n ' flowId='11960'] 61 | 62 | ##teamcity[testFinished name='testIncomplete' duration='0' flowId='11960'] 63 | 64 | ##teamcity[testStarted name='testNoAssertions' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::\PHPUnitTest::testNoAssertions' flowId='11960'] 65 | 66 | ##teamcity[testFailed name='testNoAssertions' message='This test did not perform any assertions' details=' ' flowId='11960'] 67 | 68 | ##teamcity[testFinished name='testNoAssertions' duration='0' flowId='11960'] 69 | 70 | ##teamcity[testStarted name='testAssertNotEquals' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::\PHPUnitTest::testAssertNotEquals' flowId='11960'] 71 | 72 | ##teamcity[testFailed name='testAssertNotEquals' message='Failed asserting that Array &0 (|n |'e|' => |'f|'|n 0 => |'g|'|n 1 => |'h|'|n) is identical to Array &0 (|n |'a|' => |'b|'|n |'c|' => |'d|'|n).' details=' C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php:39|n ' flowId='11960'] 73 | 74 | ##teamcity[testFinished name='testAssertNotEquals' duration='0' flowId='11960'] 75 | 76 | ##teamcity[testSuiteFinished name='PHPUnitTest' flowId='11960'] 77 | 78 | ##teamcity[testSuiteFinished name='tests/fixtures/' flowId='11960'] 79 | 80 | ##teamcity[testCount count='1' flowId='18584'] 81 | 82 | ##teamcity[testSuiteStarted name='tests/fixtures/' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::PHPUnitTest' flowId='18584'] 83 | 84 | ##teamcity[testStarted name='testDetailsHasCurrentFile' locationHint='php_qn://C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php::PHPUnitTest::testDetailsHasCurrentFile' flowId='18584'] 85 | 86 | ##teamcity[testFailed name='testDetailsHasCurrentFile' message='Invalid JSON was returned from the route.' details=' C:\Users\recca\Desktop\vscode-phpunit\vendor\laravel\framework\src\Illuminate\Foundation\Testing\TestResponse.php:434|n C:\Users\recca\Desktop\vscode-phpunit\vendor\laravel\framework\src\Illuminate\Foundation\Testing\TestResponse.php:290|n C:\Users\recca\Desktop\vscode-phpunit\tests\fixtures\PHPUnitTest.php:31|n ' flowId='18584'] 87 | 88 | ##teamcity[testFinished name='testDetailsHasCurrentFile' duration='990' flowId='18584'] 89 | 90 | ##teamcity[testSuiteFinished name='tests/fixtures/' flowId='18584'] 91 | 92 | Time: 224 ms, Memory: 4.00MB 93 | 94 | 95 | FAILURES! 96 | Tests: 11, Assertions: 5, Failures: 3, Skipped: 2, Incomplete: 2, Risky: 2. -------------------------------------------------------------------------------- /tests/fixtures/usr/bin/php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recca0120/phpunit-language-server/1c4b5e0d8801613fa363760fb61c2e2516a2e2d3/tests/fixtures/usr/bin/php -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | export function projectPath(p: string) { 4 | return resolve(__dirname, 'fixtures/project', p.replace(/\\/g, '/')).replace(/^C:/, 'c:'); 5 | } 6 | 7 | export const pathPattern = /C:\\Users\\recca\\Desktop\\vscode-phpunit\\server\\tests\\fixtures\\project\\(.+\.php)?/g; 8 | -------------------------------------------------------------------------------- /tests/phpunit/ast.test.ts: -------------------------------------------------------------------------------- 1 | import { Ast } from '../../src/phpunit/ast'; 2 | import { Filesystem, FilesystemContract } from '../../src/filesystem'; 3 | import { projectPath } from '../helpers'; 4 | import { TestNode } from '../../src/phpunit'; 5 | 6 | describe('Ast Test', () => { 7 | const files: FilesystemContract = new Filesystem(); 8 | const path: string = projectPath('tests/AssertionsTest.php'); 9 | const uri: string = files.uri(path); 10 | const ast: Ast = new Ast(); 11 | let testNodes: TestNode[] = []; 12 | beforeAll(async () => { 13 | const code: string = await files.get(path); 14 | testNodes = ast.parse(code, uri); 15 | }); 16 | 17 | it('it should be class and method name will equal to class name', () => { 18 | expect(testNodes[0]).toEqual({ 19 | name: 'Tests\\AssertionsTest', 20 | class: 'Tests\\AssertionsTest', 21 | classname: 'Tests.AssertionsTest', 22 | uri: uri, 23 | range: { 24 | end: { 25 | character: 14, 26 | line: 6, 27 | }, 28 | start: { 29 | character: 0, 30 | line: 6, 31 | }, 32 | }, 33 | }); 34 | }); 35 | 36 | it('it should be test_passed', () => { 37 | expect(testNodes[1]).toEqual({ 38 | name: 'test_passed', 39 | class: 'Tests\\AssertionsTest', 40 | classname: 'Tests.AssertionsTest', 41 | uri: uri, 42 | range: { 43 | end: { 44 | character: 22, 45 | line: 8, 46 | }, 47 | start: { 48 | character: 11, 49 | line: 8, 50 | }, 51 | }, 52 | }); 53 | }); 54 | 55 | it('it should be test_error', () => { 56 | expect(testNodes[2]).toEqual({ 57 | name: 'test_error', 58 | class: 'Tests\\AssertionsTest', 59 | classname: 'Tests.AssertionsTest', 60 | uri: uri, 61 | range: { 62 | end: { 63 | character: 21, 64 | line: 13, 65 | }, 66 | start: { 67 | character: 11, 68 | line: 13, 69 | }, 70 | }, 71 | }); 72 | }); 73 | 74 | it('it should be test_assertion_isnt_same', () => { 75 | expect(testNodes[3]).toEqual({ 76 | name: 'test_assertion_isnt_same', 77 | class: 'Tests\\AssertionsTest', 78 | classname: 'Tests.AssertionsTest', 79 | uri: uri, 80 | range: { 81 | end: { 82 | character: 35, 83 | line: 18, 84 | }, 85 | start: { 86 | character: 11, 87 | line: 18, 88 | }, 89 | }, 90 | }); 91 | }); 92 | 93 | it('it should be it_should_be_annotation_test', () => { 94 | expect(testNodes[5]).toEqual({ 95 | name: 'it_should_be_annotation_test', 96 | class: 'Tests\\AssertionsTest', 97 | classname: 'Tests.AssertionsTest', 98 | uri: uri, 99 | range: { 100 | end: { 101 | character: 39, 102 | line: 31, 103 | }, 104 | start: { 105 | character: 11, 106 | line: 31, 107 | }, 108 | }, 109 | }); 110 | }); 111 | 112 | it('it should be test_skipped', () => { 113 | expect(testNodes[6]).toEqual({ 114 | name: 'test_skipped', 115 | class: 'Tests\\AssertionsTest', 116 | classname: 'Tests.AssertionsTest', 117 | uri: uri, 118 | range: { 119 | end: { 120 | character: 23, 121 | line: 36, 122 | }, 123 | start: { 124 | character: 11, 125 | line: 36, 126 | }, 127 | }, 128 | }); 129 | }); 130 | 131 | it('it should be test_incomplete', () => { 132 | expect(testNodes[7]).toEqual({ 133 | name: 'test_incomplete', 134 | class: 'Tests\\AssertionsTest', 135 | classname: 'Tests.AssertionsTest', 136 | uri: uri, 137 | range: { 138 | end: { 139 | character: 26, 140 | line: 41, 141 | }, 142 | start: { 143 | character: 11, 144 | line: 41, 145 | }, 146 | }, 147 | }); 148 | }); 149 | 150 | it('it should be test_no_assertion', () => { 151 | expect(testNodes[8]).toEqual({ 152 | name: 'test_no_assertion', 153 | class: 'Tests\\AssertionsTest', 154 | classname: 'Tests.AssertionsTest', 155 | uri: uri, 156 | range: { 157 | end: { 158 | character: 28, 159 | line: 46, 160 | }, 161 | start: { 162 | character: 11, 163 | line: 46, 164 | }, 165 | }, 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /tests/phpunit/collection.test.ts: -------------------------------------------------------------------------------- 1 | import { Assertion, Collection, Test, Type } from '../../src/phpunit'; 2 | import { Diagnostic, PublishDiagnosticsParams, Range } from 'vscode-languageserver'; 3 | import { Filesystem, FilesystemContract } from '../../src/filesystem'; 4 | import { projectPath } from '../helpers'; 5 | 6 | describe('Collection Test', () => { 7 | const files: FilesystemContract = new Filesystem(); 8 | const path: string = projectPath('junit.xml'); 9 | const uri: string = files.uri(path); 10 | const tests: Test[] = [ 11 | { 12 | uri: uri, 13 | range: { 14 | start: { 15 | line: 56, 16 | character: 8, 17 | }, 18 | end: { 19 | line: 56, 20 | character: 38, 21 | }, 22 | }, 23 | name: 'test_throw_exception', 24 | class: 'Tests\\CalculatorTest', 25 | classname: 'Tests.CalculatorTest', 26 | time: 0.000157, 27 | type: Type.ERROR, 28 | fault: { 29 | type: 'Exception', 30 | message: 'Exception:', 31 | details: [ 32 | { 33 | uri: files.uri(projectPath('src/Calculator.php')), 34 | range: { 35 | start: { 36 | line: 20, 37 | character: 8, 38 | }, 39 | end: { 40 | line: 20, 41 | character: 28, 42 | }, 43 | }, 44 | }, 45 | ], 46 | }, 47 | }, 48 | ]; 49 | 50 | it('it should put tests and remove same tests', () => { 51 | const collect: Collection = new Collection(); 52 | 53 | const oldTests: Test[] = [ 54 | { 55 | name: 'method_1', 56 | class: 'foo', 57 | classname: 'string', 58 | uri: files.uri('foo'), 59 | range: Range.create(9, 1, 9, 1), 60 | time: 1, 61 | type: Type.PASSED, 62 | }, 63 | { 64 | name: 'method_2', 65 | class: 'foo', 66 | classname: 'string', 67 | uri: files.uri('foo'), 68 | range: Range.create(19, 1, 19, 1), 69 | time: 1, 70 | type: Type.PASSED, 71 | }, 72 | ]; 73 | 74 | collect.put(oldTests); 75 | 76 | expect(collect.get('foo')).toEqual([ 77 | { 78 | name: 'method_1', 79 | class: 'foo', 80 | classname: 'string', 81 | uri: files.uri('foo'), 82 | range: Range.create(9, 1, 9, 1), 83 | time: 1, 84 | type: Type.PASSED, 85 | }, 86 | { 87 | name: 'method_2', 88 | class: 'foo', 89 | classname: 'string', 90 | uri: files.uri('foo'), 91 | range: Range.create(19, 1, 19, 1), 92 | time: 1, 93 | type: Type.PASSED, 94 | }, 95 | ]); 96 | 97 | const newTests: Test[] = [ 98 | { 99 | name: 'method_1', 100 | class: 'foo', 101 | classname: 'string', 102 | uri: files.uri('foo'), 103 | range: Range.create(9, 1, 9, 1), 104 | time: 2, 105 | type: Type.PASSED, 106 | }, 107 | { 108 | name: 'method_3', 109 | class: 'foo', 110 | classname: 'string', 111 | uri: files.uri('foo'), 112 | range: Range.create(29, 1, 29, 1), 113 | time: 1, 114 | type: Type.PASSED, 115 | }, 116 | { 117 | name: 'method_1', 118 | class: 'bar', 119 | classname: 'string', 120 | uri: files.uri('bar'), 121 | range: Range.create(9, 1, 9, 1), 122 | time: 2, 123 | type: Type.PASSED, 124 | }, 125 | ]; 126 | 127 | collect.put(newTests); 128 | 129 | expect(collect.get('foo')).toEqual([ 130 | { 131 | name: 'method_1', 132 | class: 'foo', 133 | classname: 'string', 134 | uri: files.uri('foo'), 135 | range: Range.create(9, 1, 9, 1), 136 | time: 2, 137 | type: Type.PASSED, 138 | }, 139 | { 140 | name: 'method_2', 141 | class: 'foo', 142 | classname: 'string', 143 | uri: files.uri('foo'), 144 | range: Range.create(19, 1, 19, 1), 145 | time: 1, 146 | type: Type.PASSED, 147 | }, 148 | { 149 | name: 'method_3', 150 | class: 'foo', 151 | classname: 'string', 152 | uri: files.uri('foo'), 153 | range: Range.create(29, 1, 29, 1), 154 | time: 1, 155 | type: Type.PASSED, 156 | }, 157 | ]); 158 | 159 | expect(collect.get('bar')).toEqual([ 160 | { 161 | name: 'method_1', 162 | class: 'bar', 163 | classname: 'string', 164 | uri: files.uri('bar'), 165 | range: Range.create(9, 1, 9, 1), 166 | time: 2, 167 | type: Type.PASSED, 168 | }, 169 | ]); 170 | }); 171 | 172 | it('it should get diagnostics', () => { 173 | const collect: Collection = new Collection(); 174 | collect.put(tests); 175 | 176 | const diagnostics: Map = collect.getDiagnoics(); 177 | 178 | expect(diagnostics.get(uri)[0]).toEqual({ 179 | severity: 1, 180 | source: 'PHPUnit', 181 | message: 'Exception:', 182 | range: { 183 | end: { 184 | character: 38, 185 | line: 56, 186 | }, 187 | start: { 188 | character: 8, 189 | line: 56, 190 | }, 191 | }, 192 | relatedInformation: [ 193 | { 194 | message: 'Exception:', 195 | location: { 196 | uri: files.uri(projectPath('src/Calculator.php')), 197 | range: { 198 | end: { 199 | character: 28, 200 | line: 20, 201 | }, 202 | start: { 203 | character: 8, 204 | line: 20, 205 | }, 206 | }, 207 | }, 208 | }, 209 | ], 210 | }); 211 | }); 212 | 213 | it('it should get assertions', async () => { 214 | const collect: Collection = new Collection(); 215 | collect.put(tests); 216 | 217 | const assertionGroup: Map = collect.getAssertions(); 218 | let assertions: Assertion[] = assertionGroup.get(uri); 219 | 220 | expect(assertions[0]).toEqual({ 221 | uri: uri, 222 | range: { 223 | end: { 224 | character: 38, 225 | line: 56, 226 | }, 227 | start: { 228 | character: 8, 229 | line: 56, 230 | }, 231 | }, 232 | related: { 233 | class: 'Tests\\CalculatorTest', 234 | classname: 'Tests.CalculatorTest', 235 | name: 'test_throw_exception', 236 | time: 0.000157, 237 | type: 'error', 238 | uri: uri, 239 | fault: { 240 | message: 'Exception:', 241 | type: 'Exception', 242 | }, 243 | range: { 244 | end: { 245 | character: 38, 246 | line: 56, 247 | }, 248 | start: { 249 | character: 8, 250 | line: 56, 251 | }, 252 | }, 253 | }, 254 | }); 255 | 256 | assertions = assertionGroup.get(files.uri(projectPath('src/Calculator.php'))); 257 | 258 | expect(assertions[0]).toEqual({ 259 | uri: files.uri(projectPath('src/Calculator.php')), 260 | range: { 261 | end: { 262 | character: 28, 263 | line: 20, 264 | }, 265 | start: { 266 | character: 8, 267 | line: 20, 268 | }, 269 | }, 270 | related: { 271 | class: 'Tests\\CalculatorTest', 272 | classname: 'Tests.CalculatorTest', 273 | name: 'test_throw_exception', 274 | time: 0.000157, 275 | type: 'error', 276 | uri: uri, 277 | fault: { 278 | message: 'Exception:', 279 | type: 'Exception', 280 | }, 281 | range: { 282 | end: { 283 | character: 38, 284 | line: 56, 285 | }, 286 | start: { 287 | character: 8, 288 | line: 56, 289 | }, 290 | }, 291 | }, 292 | }); 293 | }); 294 | }); 295 | -------------------------------------------------------------------------------- /tests/phpunit/junit.test.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem } from '../../src/filesystem'; 2 | import { JUnit, Test, Type } from '../../src/phpunit'; 3 | import { parse } from 'fast-xml-parser'; 4 | import { pathPattern, projectPath } from './../helpers'; 5 | import { resolve } from 'path'; 6 | 7 | describe('JUnit Test', () => { 8 | const files = new Filesystem(); 9 | const jUnit: JUnit = new JUnit(files); 10 | const path = projectPath('tests/AssertionsTest.php'); 11 | const path2 = projectPath('tests/CalculatorTest.php'); 12 | let content: string = ''; 13 | let tests: Test[] = []; 14 | 15 | beforeAll(async () => { 16 | const jUnitFile: string = projectPath('junit.xml'); 17 | content = await files.get(jUnitFile); 18 | content = content.replace(pathPattern, (...m) => { 19 | return projectPath(m[1]); 20 | }); 21 | spyOn(files, 'get').and.returnValue(content); 22 | spyOn(files, 'unlink').and.callFake(() => {}); 23 | tests = await jUnit.parseFile(jUnitFile); 24 | }); 25 | 26 | it('test_passed', () => { 27 | expect(tests[0]).toEqual({ 28 | name: 'test_passed', 29 | class: 'Tests\\AssertionsTest', 30 | classname: 'Tests.AssertionsTest', 31 | uri: files.uri(path), 32 | range: { 33 | end: { 34 | character: 33, 35 | line: 8, 36 | }, 37 | start: { 38 | character: 4, 39 | line: 8, 40 | }, 41 | }, 42 | time: 0.007537, 43 | type: Type.PASSED, 44 | }); 45 | }); 46 | 47 | it('test_error', () => { 48 | expect(tests[1]).toEqual({ 49 | name: 'test_error', 50 | class: 'Tests\\AssertionsTest', 51 | classname: 'Tests.AssertionsTest', 52 | uri: files.uri(path), 53 | range: { 54 | end: { 55 | character: 33, 56 | line: 15, 57 | }, 58 | start: { 59 | character: 8, 60 | line: 15, 61 | }, 62 | }, 63 | time: 0.001508, 64 | type: Type.FAILURE, 65 | fault: { 66 | type: 'PHPUnit\\Framework\\ExpectationFailedException', 67 | message: 'Failed asserting that false is true.', 68 | details: [], 69 | }, 70 | }); 71 | }); 72 | 73 | it('test_assertion_isnt_same', () => { 74 | expect(tests[2]).toEqual({ 75 | name: 'test_assertion_isnt_same', 76 | class: 'Tests\\AssertionsTest', 77 | classname: 'Tests.AssertionsTest', 78 | uri: files.uri(path), 79 | range: { 80 | end: { 81 | character: 76, 82 | line: 20, 83 | }, 84 | start: { 85 | character: 8, 86 | line: 20, 87 | }, 88 | }, 89 | time: 0.001332, 90 | type: Type.FAILURE, 91 | fault: { 92 | type: 'PHPUnit\\Framework\\ExpectationFailedException', 93 | message: 94 | "Failed asserting that two arrays are identical.\n--- Expected\n+++ Actual\n@@ @@\n Array &0 (\n- 'a' => 'b'\n- 'c' => 'd'\n+ 'e' => 'f'\n+ 0 => 'g'\n+ 1 => 'h'\n )", 95 | details: [], 96 | }, 97 | }); 98 | }); 99 | 100 | it('test_risky', () => { 101 | expect(tests[3]).toEqual({ 102 | name: 'test_risky', 103 | class: 'Tests\\AssertionsTest', 104 | classname: 'Tests.AssertionsTest', 105 | uri: files.uri(path), 106 | range: { 107 | end: { 108 | character: 32, 109 | line: 23, 110 | }, 111 | start: { 112 | character: 4, 113 | line: 23, 114 | }, 115 | }, 116 | time: 0.000079, 117 | type: Type.RISKY, 118 | fault: { 119 | type: 'PHPUnit\\Framework\\RiskyTestError', 120 | message: 'Risky Test', 121 | details: [], 122 | }, 123 | }); 124 | }); 125 | 126 | it('it_should_be_annotation_test', () => { 127 | expect(tests[4]).toEqual({ 128 | name: 'it_should_be_annotation_test', 129 | class: 'Tests\\AssertionsTest', 130 | classname: 'Tests.AssertionsTest', 131 | uri: files.uri(path), 132 | range: { 133 | end: { 134 | character: 50, 135 | line: 31, 136 | }, 137 | start: { 138 | character: 4, 139 | line: 31, 140 | }, 141 | }, 142 | time: 0.000063, 143 | type: Type.PASSED, 144 | }); 145 | }); 146 | 147 | it('test_skipped', () => { 148 | expect(tests[5]).toEqual({ 149 | name: 'test_skipped', 150 | class: 'Tests\\AssertionsTest', 151 | classname: 'Tests.AssertionsTest', 152 | uri: files.uri(path), 153 | range: { 154 | end: { 155 | character: 34, 156 | line: 36, 157 | }, 158 | start: { 159 | character: 4, 160 | line: 36, 161 | }, 162 | }, 163 | time: 0.000664, 164 | type: Type.SKIPPED, 165 | fault: { 166 | type: 'PHPUnit\\Framework\\SkippedTestError', 167 | message: 'Skipped Test', 168 | details: [], 169 | }, 170 | }); 171 | }); 172 | 173 | it('test_incomplete', () => { 174 | expect(tests[6]).toEqual({ 175 | name: 'test_incomplete', 176 | class: 'Tests\\AssertionsTest', 177 | classname: 'Tests.AssertionsTest', 178 | uri: files.uri(path), 179 | range: { 180 | end: { 181 | character: 37, 182 | line: 41, 183 | }, 184 | start: { 185 | character: 4, 186 | line: 41, 187 | }, 188 | }, 189 | time: 0.000693, 190 | type: Type.SKIPPED, 191 | fault: { 192 | type: 'PHPUnit\\Framework\\SkippedTestError', 193 | message: 'Skipped Test', 194 | details: [], 195 | }, 196 | }); 197 | }); 198 | 199 | it('test_no_assertion', () => { 200 | expect(tests[7]).toEqual({ 201 | name: 'test_no_assertion', 202 | class: 'Tests\\AssertionsTest', 203 | classname: 'Tests.AssertionsTest', 204 | uri: files.uri(path), 205 | range: { 206 | end: { 207 | character: 39, 208 | line: 46, 209 | }, 210 | start: { 211 | character: 4, 212 | line: 46, 213 | }, 214 | }, 215 | time: 0.000047, 216 | type: Type.RISKY, 217 | fault: { 218 | type: 'PHPUnit\\Framework\\RiskyTestError', 219 | message: 'Risky Test', 220 | details: [], 221 | }, 222 | }); 223 | }); 224 | 225 | it('test_sum', () => { 226 | expect(tests[8]).toEqual({ 227 | name: 'test_sum', 228 | class: 'Tests\\CalculatorTest', 229 | classname: 'Tests.CalculatorTest', 230 | uri: files.uri(path2), 231 | range: { 232 | end: { 233 | character: 30, 234 | line: 17, 235 | }, 236 | start: { 237 | character: 4, 238 | line: 17, 239 | }, 240 | }, 241 | time: 0.00197, 242 | type: Type.PASSED, 243 | }); 244 | }); 245 | 246 | it('test_sum_fail', () => { 247 | expect(tests[9]).toEqual({ 248 | name: 'test_sum_fail', 249 | class: 'Tests\\CalculatorTest', 250 | classname: 'Tests.CalculatorTest', 251 | uri: files.uri(path2), 252 | range: { 253 | end: { 254 | character: 53, 255 | line: 28, 256 | }, 257 | start: { 258 | character: 8, 259 | line: 28, 260 | }, 261 | }, 262 | time: 0.000132, 263 | type: Type.FAILURE, 264 | fault: { 265 | type: 'PHPUnit\\Framework\\ExpectationFailedException', 266 | message: 'Failed asserting that 4 is identical to 3.', 267 | details: [], 268 | }, 269 | }); 270 | }); 271 | 272 | it('test_sum_item', () => { 273 | expect(tests[10]).toEqual({ 274 | name: 'test_sum_item', 275 | class: 'Tests\\CalculatorTest', 276 | classname: 'Tests.CalculatorTest', 277 | uri: files.uri(path2), 278 | range: { 279 | end: { 280 | character: 35, 281 | line: 31, 282 | }, 283 | start: { 284 | character: 4, 285 | line: 31, 286 | }, 287 | }, 288 | time: 0.000608, 289 | type: Type.PASSED, 290 | }); 291 | }); 292 | 293 | it('test_sum_item_method_not_call', () => { 294 | expect(tests[11]).toEqual({ 295 | name: 'test_sum_item_method_not_call', 296 | class: 'Tests\\CalculatorTest', 297 | classname: 'Tests.CalculatorTest', 298 | uri: files.uri(projectPath(path2)), 299 | range: { 300 | end: { 301 | character: 19, 302 | line: 14, 303 | }, 304 | start: { 305 | character: 8, 306 | line: 14, 307 | }, 308 | }, 309 | time: 0.027106, 310 | type: Type.ERROR, 311 | fault: { 312 | type: 'Mockery\\Exception\\InvalidCountException', 313 | message: 314 | 'Mockery\\Exception\\InvalidCountException: Method test() from Mockery_0_App_Item_App_Item should be called\n exactly 1 times but called 0 times.', 315 | details: [ 316 | { 317 | uri: files.uri(projectPath('vendor/mockery/mockery/library/Mockery/CountValidator/Exact.php')), 318 | range: { 319 | end: { 320 | character: 69, 321 | line: 37, 322 | }, 323 | start: { 324 | character: 12, 325 | line: 37, 326 | }, 327 | }, 328 | }, 329 | { 330 | uri: files.uri(projectPath('vendor/mockery/mockery/library/Mockery/Expectation.php')), 331 | range: { 332 | end: { 333 | character: 54, 334 | line: 308, 335 | }, 336 | start: { 337 | character: 12, 338 | line: 308, 339 | }, 340 | }, 341 | }, 342 | { 343 | uri: files.uri(projectPath('vendor/mockery/mockery/library/Mockery/ExpectationDirector.php')), 344 | range: { 345 | end: { 346 | character: 31, 347 | line: 118, 348 | }, 349 | start: { 350 | character: 16, 351 | line: 118, 352 | }, 353 | }, 354 | }, 355 | { 356 | uri: files.uri(projectPath('vendor/mockery/mockery/library/Mockery/Container.php')), 357 | range: { 358 | end: { 359 | character: 36, 360 | line: 300, 361 | }, 362 | start: { 363 | character: 12, 364 | line: 300, 365 | }, 366 | }, 367 | }, 368 | { 369 | uri: files.uri(projectPath('vendor/mockery/mockery/library/Mockery/Container.php')), 370 | range: { 371 | end: { 372 | character: 36, 373 | line: 285, 374 | }, 375 | start: { 376 | character: 12, 377 | line: 285, 378 | }, 379 | }, 380 | }, 381 | { 382 | uri: files.uri(projectPath('vendor/mockery/mockery/library/Mockery.php')), 383 | range: { 384 | end: { 385 | character: 39, 386 | line: 164, 387 | }, 388 | start: { 389 | character: 8, 390 | line: 164, 391 | }, 392 | }, 393 | }, 394 | ], 395 | }, 396 | }); 397 | }); 398 | 399 | it('test_sum_item', () => { 400 | expect(tests[12]).toEqual({ 401 | name: 'test_throw_exception', 402 | class: 'Tests\\CalculatorTest', 403 | classname: 'Tests.CalculatorTest', 404 | uri: files.uri(projectPath(path2)), 405 | range: { 406 | end: { 407 | character: 38, 408 | line: 56, 409 | }, 410 | start: { 411 | character: 8, 412 | line: 56, 413 | }, 414 | }, 415 | time: 0.000157, 416 | type: Type.ERROR, 417 | fault: { 418 | type: 'Exception', 419 | message: 'Exception:', 420 | details: [ 421 | { 422 | uri: files.uri(projectPath('src/Calculator.php')), 423 | range: { 424 | end: { 425 | character: 28, 426 | line: 20, 427 | }, 428 | start: { 429 | character: 8, 430 | line: 20, 431 | }, 432 | }, 433 | }, 434 | ], 435 | }, 436 | }); 437 | }); 438 | }); 439 | -------------------------------------------------------------------------------- /tests/phpunit/parameters.test.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem } from './../../src/filesystem'; 2 | import { Parameters } from './../../src/phpunit'; 3 | import { resolve } from 'path'; 4 | 5 | describe('Parameters Test', () => { 6 | it('it should set value', async () => { 7 | const files: Filesystem = new Filesystem(); 8 | const parameters: Parameters = new Parameters(files); 9 | 10 | parameters.setCwd(resolve(__dirname, '../fixtures/project/tests')); 11 | parameters.setRoot(resolve(__dirname, '../fixtures/project')); 12 | 13 | spyOn(files, 'tmpfile').and.returnValue('tmpfile'); 14 | 15 | parameters.set(['foo', 'bar']); 16 | 17 | expect(await parameters.all()).toEqual([ 18 | 'foo', 19 | 'bar', 20 | '-c', 21 | resolve(__dirname, '../fixtures/project/phpunit.xml.dist'), 22 | '--log-junit', 23 | 'tmpfile', 24 | ]); 25 | }); 26 | 27 | it('it should get value', () => { 28 | const files: Filesystem = new Filesystem(); 29 | const parameters: Parameters = new Parameters(files); 30 | 31 | spyOn(files, 'tmpfile').and.returnValue('tmpfile'); 32 | 33 | parameters.set(['--foo', 'bar', '--buzz']); 34 | 35 | expect(parameters.get('--foo')).toEqual('bar'); 36 | expect(parameters.get('--buzz')).toBeTruthy(); 37 | expect(parameters.get('-f')).toBeFalsy(); 38 | }); 39 | 40 | it('it should exists', () => { 41 | const files: Filesystem = new Filesystem(); 42 | const parameters: Parameters = new Parameters(files); 43 | 44 | spyOn(files, 'tmpfile').and.returnValue('tmpfile'); 45 | 46 | parameters.set(['--foo', 'bar', '--buzz']); 47 | 48 | expect(parameters.exists('--foo')).toBeTruthy(); 49 | expect(parameters.exists('--buzz')).toBeTruthy(); 50 | expect(parameters.exists('-f')).toBeFalsy(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/phpunit/phpunit.test.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem, FilesystemContract } from '../../../server/src/filesystem'; 2 | import { os, OS } from '../../src/helpers'; 3 | import { Parameters, PhpUnit } from '../../src/phpunit'; 4 | import { Process } from '../../src/process'; 5 | import { projectPath } from '../helpers'; 6 | import { resolve } from 'path'; 7 | 8 | describe('PhpUnit Test', () => { 9 | it('it should execute phpunit', async () => { 10 | const path = projectPath('tests/PHPUnitTest.php'); 11 | const command = projectPath(`vendor/bin/phpunit${os() === OS.WIN ? '.bat' : ''}`); 12 | const files: FilesystemContract = new Filesystem(); 13 | const process: Process = new Process(); 14 | const parameters: Parameters = new Parameters(files); 15 | const phpUnit = new PhpUnit(files, process, parameters); 16 | 17 | spyOn(parameters, 'all').and.returnValue([path]); 18 | spyOn(process, 'spawn').and.returnValue('output'); 19 | 20 | expect(await phpUnit.run(path)).toEqual(0); 21 | 22 | expect(phpUnit.getOutput()).toEqual('output'); 23 | expect(phpUnit.getTests()).toEqual([]); 24 | 25 | expect((process.spawn as jasmine.Spy).calls.argsFor(0)).toEqual([ 26 | { 27 | arguments: [path], 28 | command: command, 29 | title: '', 30 | }, 31 | ]); 32 | }); 33 | 34 | it('it should execute phpunit with customize binary and arguments', async () => { 35 | const path = projectPath('tests/PHPUnitTest.php'); 36 | const command = projectPath('vendor/bin/unittest'); 37 | const files: FilesystemContract = new Filesystem(); 38 | const process: Process = new Process(); 39 | const parameters: Parameters = new Parameters(files); 40 | const phpUnit = new PhpUnit(files, process, parameters); 41 | 42 | spyOn(parameters, 'set').and.callThrough(); 43 | spyOn(parameters, 'all').and.returnValue([path]); 44 | spyOn(process, 'spawn').and.returnValue('output'); 45 | 46 | expect( 47 | await phpUnit 48 | .setBinary(command) 49 | .setDefault(['foo', 'bar']) 50 | .run(path, ['-c', 'bootstrap.php']) 51 | ).toEqual(0); 52 | 53 | expect(phpUnit.getOutput()).toEqual('output'); 54 | expect(phpUnit.getTests()).toEqual([]); 55 | 56 | expect((parameters.set as jasmine.Spy).calls.argsFor(0)).toEqual([['foo', 'bar', '-c', 'bootstrap.php', path]]); 57 | 58 | expect((process.spawn as jasmine.Spy).calls.argsFor(0)).toEqual([ 59 | { 60 | arguments: [path], 61 | command: command, 62 | title: '', 63 | }, 64 | ]); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/process.test.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'vscode-languageserver'; 2 | import { Process } from './../src/process'; 3 | 4 | describe('Process Test', () => { 5 | it('echo hello world', async () => { 6 | const process: Process = new Process(); 7 | 8 | expect( 9 | await process.spawn({ 10 | command: 'echo', 11 | arguments: ['hello world'], 12 | title: '', 13 | }) 14 | ).toEqual('hello world'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/providers/codelens-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { CodeLens, TextDocument } from 'vscode-languageserver'; 2 | import { CodeLensProvider } from '../../src/providers'; 3 | import { Filesystem, FilesystemContract } from '../../src/filesystem'; 4 | import { projectPath } from '../helpers'; 5 | import { readFileSync } from 'fs'; 6 | import { resolve } from 'path'; 7 | import { Runner } from '../../src/runner'; 8 | 9 | describe('CodeLensProvider Test', () => { 10 | const files: FilesystemContract = new Filesystem(); 11 | const path: string = projectPath('tests/AssertionsTest.php').replace(/^C:/, 'c:'); 12 | const uri: string = files.uri(path); 13 | let codeLens: CodeLens[] = []; 14 | 15 | beforeAll(async () => { 16 | const runner = new Runner(); 17 | const codeLensProvider: CodeLensProvider = new CodeLensProvider(runner); 18 | const content: string = await files.get(path); 19 | const textDocument: TextDocument = TextDocument.create(uri, 'php', 0.1, content); 20 | codeLens = codeLensProvider.provideCodeLenses(textDocument); 21 | }); 22 | 23 | it('it should resolve class AssertionsTest', () => { 24 | expect(codeLens[0]).toEqual({ 25 | command: { 26 | arguments: [uri, path, []], 27 | command: 'phpunit.test.file', 28 | title: 'Run Test', 29 | }, 30 | data: { 31 | textDocument: { 32 | uri: uri, 33 | }, 34 | }, 35 | range: { 36 | end: { 37 | character: 14, 38 | line: 6, 39 | }, 40 | start: { 41 | character: 0, 42 | line: 6, 43 | }, 44 | }, 45 | }); 46 | }); 47 | 48 | it('it should resolve method testPassed codelens', () => { 49 | expect(codeLens[1]).toEqual({ 50 | command: { 51 | arguments: [uri, path, ['--filter', '^.*::test_passed$']], 52 | command: 'phpunit.test', 53 | title: 'Run Test', 54 | }, 55 | data: { 56 | textDocument: { 57 | uri: uri, 58 | }, 59 | }, 60 | range: { 61 | end: { 62 | character: 22, 63 | line: 8, 64 | }, 65 | start: { 66 | character: 11, 67 | line: 8, 68 | }, 69 | }, 70 | }); 71 | }); 72 | 73 | it('it should resolve method test_error', () => { 74 | expect(codeLens[2]).toEqual({ 75 | command: { 76 | arguments: [uri, path, ['--filter', '^.*::test_error$']], 77 | command: 'phpunit.test', 78 | title: 'Run Test', 79 | }, 80 | data: { 81 | textDocument: { 82 | uri: uri, 83 | }, 84 | }, 85 | range: { 86 | end: { 87 | character: 21, 88 | line: 13, 89 | }, 90 | start: { 91 | character: 11, 92 | line: 13, 93 | }, 94 | }, 95 | }); 96 | }); 97 | 98 | it('it should resolve method test_assertion_isnt_same', () => { 99 | expect(codeLens[3]).toEqual({ 100 | command: { 101 | arguments: [uri, path, ['--filter', '^.*::test_assertion_isnt_same$']], 102 | command: 'phpunit.test', 103 | title: 'Run Test', 104 | }, 105 | data: { 106 | textDocument: { 107 | uri: uri, 108 | }, 109 | }, 110 | range: { 111 | end: { 112 | character: 35, 113 | line: 18, 114 | }, 115 | start: { 116 | character: 11, 117 | line: 18, 118 | }, 119 | }, 120 | }); 121 | }); 122 | 123 | it('it should resolve method test_risky', () => { 124 | expect(codeLens[4]).toEqual({ 125 | command: { 126 | arguments: [uri, path, ['--filter', '^.*::test_risky$']], 127 | command: 'phpunit.test', 128 | title: 'Run Test', 129 | }, 130 | data: { 131 | textDocument: { 132 | uri: uri, 133 | }, 134 | }, 135 | range: { 136 | end: { 137 | character: 21, 138 | line: 23, 139 | }, 140 | start: { 141 | character: 11, 142 | line: 23, 143 | }, 144 | }, 145 | }); 146 | }); 147 | 148 | it('it should resolve method it_should_be_annotation_test', () => { 149 | expect(codeLens[5]).toEqual({ 150 | command: { 151 | arguments: [uri, path, ['--filter', '^.*::it_should_be_annotation_test$']], 152 | command: 'phpunit.test', 153 | title: 'Run Test', 154 | }, 155 | data: { 156 | textDocument: { 157 | uri: uri, 158 | }, 159 | }, 160 | range: { 161 | end: { 162 | character: 39, 163 | line: 31, 164 | }, 165 | start: { 166 | character: 11, 167 | line: 31, 168 | }, 169 | }, 170 | }); 171 | }); 172 | 173 | it('it should resolve method test_skipped', () => { 174 | expect(codeLens[6]).toEqual({ 175 | command: { 176 | arguments: [uri, path, ['--filter', '^.*::test_skipped$']], 177 | command: 'phpunit.test', 178 | title: 'Run Test', 179 | }, 180 | data: { 181 | textDocument: { 182 | uri: uri, 183 | }, 184 | }, 185 | range: { 186 | end: { 187 | character: 23, 188 | line: 36, 189 | }, 190 | start: { 191 | character: 11, 192 | line: 36, 193 | }, 194 | }, 195 | }); 196 | }); 197 | 198 | it('it should resolve method test_incomplete', () => { 199 | expect(codeLens[7]).toEqual({ 200 | command: { 201 | arguments: [uri, path, ['--filter', '^.*::test_incomplete$']], 202 | command: 'phpunit.test', 203 | title: 'Run Test', 204 | }, 205 | data: { 206 | textDocument: { 207 | uri: uri, 208 | }, 209 | }, 210 | range: { 211 | end: { 212 | character: 26, 213 | line: 41, 214 | }, 215 | start: { 216 | character: 11, 217 | line: 41, 218 | }, 219 | }, 220 | }); 221 | }); 222 | 223 | it('it should resolve method test_no_assertion', () => { 224 | expect(codeLens[8]).toEqual({ 225 | command: { 226 | arguments: [uri, path, ['--filter', '^.*::test_no_assertion$']], 227 | command: 'phpunit.test', 228 | title: 'Run Test', 229 | }, 230 | data: { 231 | textDocument: { 232 | uri: uri, 233 | }, 234 | }, 235 | range: { 236 | end: { 237 | character: 28, 238 | line: 46, 239 | }, 240 | start: { 241 | character: 11, 242 | line: 46, 243 | }, 244 | }, 245 | }); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /tests/providers/document-symbol-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSymbolProvider } from '../../src/providers'; 2 | import { Filesystem, FilesystemContract } from '../../src/filesystem'; 3 | import { projectPath } from '../helpers'; 4 | import { SymbolInformation, SymbolKind, TextDocument } from 'vscode-languageserver'; 5 | 6 | describe('DocumentSymbolProvider Test', () => { 7 | const path: string = projectPath('tests/AssertionsTest.php'); 8 | const files: FilesystemContract = new Filesystem(); 9 | let symbolInformations: SymbolInformation[] = []; 10 | 11 | beforeAll(async () => { 12 | const documentSymbolProvider: DocumentSymbolProvider = new DocumentSymbolProvider(); 13 | const content = await files.get(path); 14 | const textDocument: TextDocument = TextDocument.create(path, 'php', 0.1, content); 15 | symbolInformations = documentSymbolProvider.provideDocumentSymbols(textDocument); 16 | }); 17 | 18 | it('it should resolve class AssertionsTest', () => { 19 | expect(symbolInformations[0]).toEqual({ 20 | kind: SymbolKind.Class, 21 | location: { 22 | range: { 23 | end: { 24 | character: 14, 25 | line: 6, 26 | }, 27 | start: { 28 | character: 0, 29 | line: 6, 30 | }, 31 | }, 32 | uri: path, 33 | }, 34 | name: 'AssertionsTest', 35 | }); 36 | }); 37 | 38 | it('it should resolve method test_passed', () => { 39 | expect(symbolInformations[1]).toEqual({ 40 | kind: SymbolKind.Method, 41 | location: { 42 | range: { 43 | end: { 44 | character: 22, 45 | line: 8, 46 | }, 47 | start: { 48 | character: 11, 49 | line: 8, 50 | }, 51 | }, 52 | uri: path, 53 | }, 54 | name: 'test_passed', 55 | }); 56 | }); 57 | 58 | it('it should resolve method test_error', () => { 59 | expect(symbolInformations[2]).toEqual({ 60 | kind: SymbolKind.Method, 61 | location: { 62 | range: { 63 | end: { 64 | character: 21, 65 | line: 13, 66 | }, 67 | start: { 68 | character: 11, 69 | line: 13, 70 | }, 71 | }, 72 | uri: path, 73 | }, 74 | name: 'test_error', 75 | }); 76 | }); 77 | 78 | it('it should resolve method test_assertion_isnt_same', () => { 79 | expect(symbolInformations[3]).toEqual({ 80 | kind: SymbolKind.Method, 81 | location: { 82 | range: { 83 | end: { 84 | character: 35, 85 | line: 18, 86 | }, 87 | start: { 88 | character: 11, 89 | line: 18, 90 | }, 91 | }, 92 | uri: path, 93 | }, 94 | name: 'test_assertion_isnt_same', 95 | }); 96 | }); 97 | 98 | it('it should resolve method test_risky', () => { 99 | expect(symbolInformations[4]).toEqual({ 100 | kind: SymbolKind.Method, 101 | location: { 102 | range: { 103 | end: { 104 | character: 21, 105 | line: 23, 106 | }, 107 | start: { 108 | character: 11, 109 | line: 23, 110 | }, 111 | }, 112 | uri: path, 113 | }, 114 | name: 'test_risky', 115 | }); 116 | }); 117 | 118 | it('it should resolve method it_should_be_annotation_test', () => { 119 | expect(symbolInformations[5]).toEqual({ 120 | kind: SymbolKind.Method, 121 | location: { 122 | range: { 123 | end: { 124 | character: 39, 125 | line: 31, 126 | }, 127 | start: { 128 | character: 11, 129 | line: 31, 130 | }, 131 | }, 132 | uri: path, 133 | }, 134 | name: 'it_should_be_annotation_test', 135 | }); 136 | }); 137 | 138 | it('it should resolve method test_skipped', () => { 139 | expect(symbolInformations[6]).toEqual({ 140 | kind: SymbolKind.Method, 141 | location: { 142 | range: { 143 | end: { 144 | character: 23, 145 | line: 36, 146 | }, 147 | start: { 148 | character: 11, 149 | line: 36, 150 | }, 151 | }, 152 | uri: path, 153 | }, 154 | name: 'test_skipped', 155 | }); 156 | }); 157 | 158 | it('it should resolve method test_incomplete', () => { 159 | expect(symbolInformations[7]).toEqual({ 160 | kind: SymbolKind.Method, 161 | location: { 162 | range: { 163 | end: { 164 | character: 26, 165 | line: 41, 166 | }, 167 | start: { 168 | character: 11, 169 | line: 41, 170 | }, 171 | }, 172 | uri: path, 173 | }, 174 | name: 'test_incomplete', 175 | }); 176 | }); 177 | 178 | it('it should resolve method test_no_assertion', () => { 179 | expect(symbolInformations[8]).toEqual({ 180 | kind: SymbolKind.Method, 181 | location: { 182 | range: { 183 | end: { 184 | character: 28, 185 | line: 46, 186 | }, 187 | start: { 188 | character: 11, 189 | line: 46, 190 | }, 191 | }, 192 | uri: path, 193 | }, 194 | name: 'test_no_assertion', 195 | }); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "../client/server", 6 | "lib": ["es6"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "noUnusedLocals": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedParameters": true 15 | }, 16 | "exclude": ["node_modules", "tests", "master", "dist"] 17 | } 18 | --------------------------------------------------------------------------------