├── test ├── testdata │ └── sampleProject │ │ ├── specs │ │ ├── sample.cpt │ │ └── example.spec │ │ ├── manifest.json │ │ ├── env │ │ └── default │ │ │ ├── js.properties │ │ │ └── default.properties │ │ ├── .gitignore │ │ ├── package.json │ │ └── tests │ │ └── step_implementation.js ├── commands │ ├── test_command.bat │ ├── test_command.cmd │ ├── test_command │ └── test_command.exe ├── gauge.test.ts ├── index.ts ├── references.test.ts ├── clients.test.ts ├── runTest.ts ├── config │ └── gaugeConfig.test.ts ├── lineProcessors.test.ts ├── execution │ ├── outputChannel.test.ts │ ├── execution.test.ts │ └── runArgs.test.ts ├── project.test.ts └── cli.test.ts ├── images ├── format.jpg ├── newProj.jpg ├── reports.jpg ├── runSpec.jpg ├── symbols.jpg ├── debugSpec.jpg ├── explorer.jpg ├── autocomplete.jpg ├── diagnostics.jpg ├── gauge-icon.png ├── references.jpg ├── testExplorer.jpg ├── gotoDefinition.jpg └── documentSymbols.jpg ├── .gitignore ├── publish.sh ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── language-configuration.json ├── tsconfig.json ├── .github ├── dependabot.yml ├── issue_template.md └── workflows │ ├── githubRelease.yml │ └── vscode.yml ├── src ├── gaugeClients.ts ├── types │ └── fileListItem.ts ├── gaugeState.ts ├── util.ts ├── protocol │ ├── gauge.proposed.md │ └── gauge.proposed.ts ├── config │ ├── gaugeConfig.ts │ ├── gaugeProjectConfig.ts │ └── configProvider.ts ├── terminal │ └── terminal.ts ├── execution │ ├── lineBuffer.ts │ ├── executionConfig.ts │ ├── lineProcessors.ts │ ├── outputChannel.ts │ ├── runArgs.ts │ └── debug.ts ├── project │ ├── mavenProject.ts │ ├── gradleProject.ts │ ├── gaugeProject.ts │ └── projectFactory.ts ├── welcomeNotifications.ts ├── refactor │ └── workspaceEditor.ts ├── gaugeWorkspace.proposed.ts ├── gaugeReference.ts ├── constants.ts ├── file │ └── specificationFileProvider.ts ├── annotator │ └── generateStub.ts ├── init │ └── projectInit.ts ├── extension.ts ├── cli.ts ├── semanticTokensProvider.ts ├── explorer │ └── specExplorer.ts └── gaugeWorkspace.ts ├── resources ├── dark │ ├── icon-list.svg │ ├── document.svg │ ├── folder.svg │ ├── boolean.svg │ ├── dependency.svg │ ├── play.svg │ ├── number.svg │ └── string.svg └── light │ ├── icon-list.svg │ ├── document.svg │ ├── folder.svg │ ├── boolean.svg │ ├── dependency.svg │ ├── play.svg │ ├── number.svg │ └── string.svg ├── .vscodeignore ├── release.sh ├── run_test.ps1 ├── LICENSE ├── tslint.json ├── webpack.config.js ├── snippets └── gauge.json ├── CODE_OF_CONDUCT.md └── README.md /test/testdata/sampleProject/specs/sample.cpt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/commands/test_command.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Success: %1 -------------------------------------------------------------------------------- /test/commands/test_command.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Success: %1 -------------------------------------------------------------------------------- /test/commands/test_command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Success: \"${1}\"" -------------------------------------------------------------------------------- /images/format.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/format.jpg -------------------------------------------------------------------------------- /images/newProj.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/newProj.jpg -------------------------------------------------------------------------------- /images/reports.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/reports.jpg -------------------------------------------------------------------------------- /images/runSpec.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/runSpec.jpg -------------------------------------------------------------------------------- /images/symbols.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/symbols.jpg -------------------------------------------------------------------------------- /images/debugSpec.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/debugSpec.jpg -------------------------------------------------------------------------------- /images/explorer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/explorer.jpg -------------------------------------------------------------------------------- /images/autocomplete.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/autocomplete.jpg -------------------------------------------------------------------------------- /images/diagnostics.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/diagnostics.jpg -------------------------------------------------------------------------------- /images/gauge-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/gauge-icon.png -------------------------------------------------------------------------------- /images/references.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/references.jpg -------------------------------------------------------------------------------- /images/testExplorer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/testExplorer.jpg -------------------------------------------------------------------------------- /images/gotoDefinition.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/gotoDefinition.jpg -------------------------------------------------------------------------------- /images/documentSymbols.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/images/documentSymbols.jpg -------------------------------------------------------------------------------- /test/commands/test_command.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getgauge/gauge-vscode/HEAD/test/commands/test_command.exe -------------------------------------------------------------------------------- /test/testdata/sampleProject/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Language": "js", 3 | "Plugins": [ 4 | "html-report" 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | *.vsix 4 | .vscode-test 5 | reports 6 | logs 7 | .gauge 8 | .idea 9 | welcome.html 10 | .DS_Store 11 | test/testdata/**/.vscode/ -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | version=$(ls artifacts/gauge-*.vsix | sed "s/^artifacts\/gauge-\([^;]*\).vsix/\1/") 2 | npm install 3 | npm run publish -- --packagePath artifacts/gauge-$version.vsix -p $VS_PAT -------------------------------------------------------------------------------- /test/testdata/sampleProject/env/default/js.properties: -------------------------------------------------------------------------------- 1 | #js.properties 2 | #settings related to gauge-js. 3 | 4 | test_timeout = 10000 5 | 6 | # Change this to true to enable debugging support 7 | DEBUG = false 8 | -------------------------------------------------------------------------------- /test/testdata/sampleProject/.gitignore: -------------------------------------------------------------------------------- 1 | # Gauge - metadata dir 2 | .gauge 3 | 4 | # Gauge - log files dir 5 | logs 6 | 7 | # Gauge - reports dir 8 | reports 9 | 10 | # Gauge - JavaScript node dependencies 11 | node_modules 12 | 13 | -------------------------------------------------------------------------------- /test/testdata/sampleProject/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gauge-js-template", 3 | "version": "0.0.1", 4 | "description": "Starter template for writing JavaScript tests for Gauge", 5 | "scripts": { 6 | "test": "gauge specs/" 7 | }, 8 | "dependencies": {} 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "ms-vscode.vscode-typescript-tslint-plugin" 7 | 8 | ] 9 | } -------------------------------------------------------------------------------- /test/gauge.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | 5 | suite('Gauge Extension Tests', () => { 6 | test('should activate when manifest file found in path', () => { 7 | assert.ok(vscode.extensions.getExtension('getgauge.gauge').isActive); 8 | }); 9 | }); -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | }, 5 | "autoClosingPairs": [ 6 | [ 7 | "<", 8 | ">" 9 | ], 10 | [ 11 | "\"", 12 | "\"" 13 | ] 14 | ], 15 | "surroundingPairs": [ 16 | [ 17 | "<", 18 | ">" 19 | ], 20 | [ 21 | "\"", 22 | "\"" 23 | ] 24 | ] 25 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "out", 7 | "lib": ["es2016"], 8 | "sourceMap": true, 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true, 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: npm 12 | directory: "/" 13 | schedule: 14 | interval: monthly 15 | groups: 16 | npm: 17 | patterns: 18 | - "*" -------------------------------------------------------------------------------- /src/gaugeClients.ts: -------------------------------------------------------------------------------- 1 | import { GaugeProject } from "./project/gaugeProject"; 2 | import { LanguageClient } from "vscode-languageclient/node"; 3 | 4 | export class GaugeClients extends Map { 5 | 6 | get(fsPath: string): { project: GaugeProject, client: LanguageClient } { 7 | for (const cp of this.values()) { 8 | if (cp.project.hasFile(fsPath)) return cp; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /resources/dark/icon-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/light/icon-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .gitignore 3 | tsconfig.json 4 | tslint.json 5 | **/*.vsix 6 | **/*.zip 7 | **/*.sh 8 | test/** 9 | integration-test/** 10 | .vscode-test/** 11 | .travis.yml 12 | **/*.gif 13 | **/*.jpg 14 | out/test/** 15 | out/src/** 16 | out/integration-test/** 17 | !images/gauge-icon.png 18 | images/** 19 | .vscode-test/** 20 | src/** 21 | **/*.map 22 | package-lock.json 23 | webpack.config.js 24 | appveyor.yml 25 | *.ps1 26 | node_modules 27 | bin/** 28 | .github/** 29 | README.md 30 | .idea/ -------------------------------------------------------------------------------- /src/types/fileListItem.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { QuickPickItem } from 'vscode'; 4 | import { COPY_TO_CLIPBOARD } from '../constants'; 5 | 6 | export class FileListItem implements QuickPickItem { 7 | label: string; 8 | description: string; 9 | value: string; 10 | 11 | constructor(l: string, d: string, v: string) { 12 | this.label = l; 13 | this.description = d; 14 | this.value = v; 15 | } 16 | 17 | isCopyToClipBoard() { 18 | return this.value === COPY_TO_CLIPBOARD; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/light/document.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$GITHUB_TOKEN" ]; then 4 | echo "GITHUB_TOKEN is not set." 5 | echo "Please create an personal access token with repo:public_repo scopes." 6 | exit 1 7 | fi 8 | 9 | curl \ 10 | -X POST \ 11 | -H "Authorization: token $GITHUB_TOKEN" \ 12 | -H "Accept: application/vnd.github.ant-man-preview+json" \ 13 | -H "Content-Type: application/json" \ 14 | https://api.github.com/repos/getgauge/gauge-vscode/deployments \ 15 | --data '{"ref": "master", "required_contexts": [], "environment": "production"}' 16 | -------------------------------------------------------------------------------- /src/gaugeState.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Disposable, ExtensionContext } from "vscode"; 4 | import { LAST_REPORT_PATH } from "./constants"; 5 | 6 | export class GaugeState extends Disposable { 7 | constructor(private context: ExtensionContext) { 8 | super(() => this.dispose()); 9 | } 10 | 11 | setReportPath(reportPath: string) { 12 | this.context.workspaceState.update(LAST_REPORT_PATH, reportPath); 13 | } 14 | 15 | getReportPath(): string { 16 | return this.context.workspaceState.get(LAST_REPORT_PATH); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { TextEditor } from 'vscode'; 4 | import { spawnSync, exec } from 'child_process'; 5 | import { platform } from 'os'; 6 | 7 | export function getActiveGaugeDocument(activeTextEditor: TextEditor): Promise { 8 | return new Promise((resolve) => { 9 | if (activeTextEditor && activeTextEditor.document.languageId === "gauge") { 10 | resolve(activeTextEditor.document.uri.fsPath); 11 | } 12 | }); 13 | } 14 | 15 | export function hasActiveGaugeDocument(activeTextEditor: TextEditor): boolean { 16 | return activeTextEditor && activeTextEditor.document.languageId === "gauge"; 17 | } 18 | -------------------------------------------------------------------------------- /resources/dark/document.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /run_test.ps1: -------------------------------------------------------------------------------- 1 | $env:GAUGE_PREFIX="C:\GAUGE" 2 | 3 | mkdir $env:GAUGE_PREFIX 4 | 5 | Invoke-WebRequest -Uri "https://raw.githubusercontent.com/getgauge/infrastructure/master/nightly_scripts/install_latest_gauge_nightly.ps1" -OutFile install_latest_gauge_nightly.ps1 6 | .\install_latest_gauge_nightly.ps1 7 | 8 | $env:Path = "$env:GAUGE_PREFIX;" + [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") 9 | 10 | gauge config gauge_repository_url https://raw.githubusercontent.com/getgauge/gauge-nightly-repository/master/ 11 | 12 | gauge.exe install js 13 | gauge.exe install html-report 14 | gauge.exe version 15 | 16 | npm run build 17 | 18 | npm test -------------------------------------------------------------------------------- /test/testdata/sampleProject/specs/example.spec: -------------------------------------------------------------------------------- 1 | Specification Heading 2 | ===================== 3 | 4 | * Vowels in English language are "aeiou". 5 | 6 | Vowel counts in single word 7 | --------------------------- 8 | 9 | tags: single word 10 | 11 | * The word "gauge" has "3" vowels. 12 | 13 | Vowel counts in multiple word 14 | ----------------------------- 15 | 16 | This is the second scenario in this specification 17 | 18 | Here's a step that takes a table 19 | 20 | * Almost all words have vowels 21 | |Word |Vowel Count| 22 | |------|-----------| 23 | |Gauge |3 | 24 | |Mingle|2 | 25 | |Snap |1 | 26 | |GoCD |1 | 27 | |Rhythm|0 | 28 | -------------------------------------------------------------------------------- /src/protocol/gauge.proposed.md: -------------------------------------------------------------------------------- 1 | #### Gauge Workspace 2 | 3 | _Client Capabilities_: 4 | 5 | The client sets the following capability if it is supporting workspace save files request. 6 | 7 | \`\`\`ts 8 | /** 9 | * The client supports saveFiles request sent from server to client 10 | */ 11 | saveFiles?: boolean; 12 | \`\`\` 13 | 14 | ##### SaveFiles Request 15 | 16 | The `workspace/saveFiles` request is sent from the client to the server to inform the client about workspace folder configuration changes. 17 | 18 | _Request_: 19 | 20 | * method: 'workspace/saveFiles' 21 | * params: null 22 | 23 | _Response_: 24 | 25 | result: void | null. 26 | error: code and message set in case an exception happens during the request. 27 | 28 | -------------------------------------------------------------------------------- /resources/light/folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/dark/folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/sampleProject/env/default/default.properties: -------------------------------------------------------------------------------- 1 | # default.properties 2 | # properties set here will be available to the test execution as environment variables 3 | 4 | # sample_key = sample_value 5 | 6 | #The path to the gauge reports directory. Should be either relative to the project directory or an absolute path 7 | gauge_reports_dir = reports 8 | 9 | #Set as false if gauge reports should not be overwritten on each execution. A new time-stamped directory will be created on each execution. 10 | overwrite_reports = true 11 | 12 | # Set to false to disable screenshots on failure in reports. 13 | screenshot_on_failure = true 14 | 15 | # The path to the gauge logs directory. Should be either relative to the project directory or an absolute path 16 | logs_directory = logs -------------------------------------------------------------------------------- /src/protocol/gauge.proposed.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, HandlerResult, RequestHandler0, RequestType0 } from 'vscode-languageclient'; 2 | 3 | export interface GaugeClientCapabilities { 4 | /** 5 | * The client supports saveFiles request sent from server to client 6 | */ 7 | saveFiles?: boolean; 8 | } 9 | 10 | /** 11 | * The `workspace/saveFiles` notification is sent from the server to the client to save all open files in the workspace. 12 | */ 13 | export namespace SaveFilesRequest { 14 | export const type = new RequestType0('workspace/saveFiles'); 15 | export type HandlerSignature = RequestHandler0; 16 | export type MiddlewareSignature = (token: CancellationToken, next: HandlerSignature) => HandlerResult; 17 | } 18 | -------------------------------------------------------------------------------- /resources/dark/boolean.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/dark/dependency.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/boolean.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/dependency.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/gaugeConfig.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { homedir } from "os"; 3 | 4 | export default class GaugeConfig { 5 | private _platform: string; 6 | constructor(platform: string) { 7 | this._platform = platform; 8 | } 9 | 10 | pluginsPath() { 11 | let gaugeHome = this.homeDir(); 12 | return join(gaugeHome, 'plugins'); 13 | } 14 | 15 | private homeDir() { 16 | let customGaugeHomeDir = process.env.GAUGE_HOME; 17 | if (customGaugeHomeDir !== undefined) { 18 | return customGaugeHomeDir; 19 | } 20 | if (this._platform.match(/win\d+/i)) { 21 | let appDataDir = process.env.APPDATA; 22 | return join(appDataDir, 'Gauge'); 23 | } 24 | return join(homedir(), '.gauge'); 25 | } 26 | } -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import {glob} from 'glob'; 4 | 5 | export function run(testsRoot: string, cb: (error: any, failures?: number) => void): void { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', color: true, 9 | }); 10 | 11 | glob('**/**.test.js', {cwd: testsRoot}) 12 | .then(files => { 13 | // Add files to the test suite 14 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 15 | 16 | try { 17 | // Run the mocha test 18 | mocha 19 | .run(failures => { 20 | cb(null, failures); 21 | }); 22 | 23 | } catch (err) { 24 | cb(err); 25 | } 26 | } 27 | ).catch(err => { 28 | return cb(err); 29 | }); 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gauge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "quotemark": false, 9 | "indent": [ 10 | true, 11 | "spaces", 12 | 4 13 | ], 14 | "eofline": false, 15 | "trailing-comma":false, 16 | "ordered-imports":false, 17 | "curly": false, 18 | "object-literal-sort-keys":false, 19 | "ban-types": [ 20 | "Function" 21 | ], 22 | "no-empty":false, 23 | "no-shadowed-variable":false, 24 | "variable-name": [true, "allow-leading-underscore"], 25 | "member-access":false, 26 | "interface-name":false, 27 | "member-ordering":false, 28 | "object-literal-shorthand":false, 29 | "prefer-const":false, 30 | "array-type":false, 31 | "no-console":false, 32 | "max-classes-per-file": [true, 5], 33 | "no-namespace": false, 34 | "callable-types":false, 35 | "prefer-for-of":false, 36 | "no-unnecessary-initializer":false 37 | }, 38 | "rulesDirectory": [] 39 | } -------------------------------------------------------------------------------- /src/terminal/terminal.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Disposable, window, ExtensionContext, commands, Terminal } from "vscode"; 4 | import { GaugeVSCodeCommands } from "../constants"; 5 | 6 | let terminalStack: Terminal[] = []; 7 | 8 | export class TerminalProvider extends Disposable { 9 | private readonly _context: ExtensionContext; 10 | private readonly _disposable: Disposable; 11 | constructor(context: ExtensionContext) { 12 | super(() => this.dispose()); 13 | this._context = context; 14 | this._disposable = Disposable.from( 15 | commands.registerCommand(GaugeVSCodeCommands.ExecuteInTerminal, (text: string) => { 16 | terminalStack.push(window.createTerminal('gauge install')); 17 | getLatestTerminal().show(); 18 | getLatestTerminal().sendText(text); 19 | setTimeout( 20 | () => window.showInformationMessage(`Please reload the project after Gauge is installed!`) 21 | , 1000); 22 | } 23 | )); 24 | } 25 | } 26 | 27 | function getLatestTerminal() { 28 | return terminalStack[terminalStack.length - 1]; 29 | } 30 | -------------------------------------------------------------------------------- /src/execution/lineBuffer.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export class LineBuffer { 4 | private buf: string = ''; 5 | private lineListeners: { (line: string): void; }[] = []; 6 | private lastListeners: { (last: string): void; }[] = []; 7 | 8 | append(chunk: string) { 9 | this.buf += chunk; 10 | do { 11 | const idx = this.buf.indexOf('\n'); 12 | if (idx === -1) { 13 | break; 14 | } 15 | 16 | this.fireLine(this.buf.substring(0, idx)); 17 | this.buf = this.buf.substring(idx + 1); 18 | } while (true); 19 | } 20 | 21 | done() { 22 | this.fireDone(this.buf !== '' ? this.buf : null); 23 | } 24 | 25 | private fireLine(line: string) { 26 | this.lineListeners.forEach((listener) => listener(line)); 27 | } 28 | 29 | private fireDone(last: string) { 30 | this.lastListeners.forEach((listener) => listener(last)); 31 | } 32 | 33 | onLine(listener: (line: string) => void) { 34 | this.lineListeners.push(listener); 35 | } 36 | 37 | onDone(listener: (last: string) => void) { 38 | this.lastListeners.push(listener); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 25 | 26 | ### Describe the bug 27 | 28 | 29 | ### What command(s) did you run when you found the bug? 30 | 31 | For e.g. 32 | ``` 33 | gauge run specs 34 | ``` 35 | 36 | ### Output, stack trace or logs related to the bug 37 | 38 | ### Versions 39 | 40 | #### Gauge (Output of `gauge -v`) 41 | 42 | #### Node.js/Java/Python/.Net/Ruby version 43 | 44 | #### Operating System information 45 | 46 | #### IDE information 47 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | 5 | 6 | { 7 | "name": "Launch Extension", 8 | "type": "extensionHost", 9 | "request": "launch", 10 | "runtimeExecutable": "${execPath}", 11 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 12 | "stopOnEntry": false, 13 | "sourceMaps": true, 14 | "outFiles": ["${workspaceRoot}/out/**/*.js"] 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test", "${workspaceRoot}/test/testdata/sampleProject" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": [ "${workspaceRoot}/out/test/**/*.js" ], 25 | "preLaunchTask": "npm" 26 | } 27 | ], 28 | "compounds": [{ 29 | "name": "Extension + Debug server", 30 | "configurations": ["Launch Extension", "Launch as server"] 31 | }] 32 | } -------------------------------------------------------------------------------- /src/project/mavenProject.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { execSync } from 'child_process'; 3 | import { window } from 'vscode'; 4 | import { CLI } from '../cli'; 5 | import { GaugeProject } from './gaugeProject'; 6 | import { GAUGE_CUSTOM_CLASSPATH } from '../constants'; 7 | 8 | export class MavenProject extends GaugeProject { 9 | constructor(projectRoot: string, manifest: any) { 10 | super(projectRoot, manifest); 11 | } 12 | 13 | public getExecutionCommand(cli: CLI) { 14 | return cli.mavenCommand(); 15 | } 16 | 17 | public equals(o: Object): boolean { 18 | if (o == null) return false; 19 | if (!(o instanceof MavenProject)) return false; 20 | if (o === this) return true; 21 | return this.root() === (o as MavenProject).root(); 22 | } 23 | 24 | public envs(cli: CLI): NodeJS.ProcessEnv { 25 | try { 26 | let classpath = execSync(`${this.getExecutionCommand(cli)?.command} -q gauge:classpath`, {cwd: this.root()}); 27 | return {[GAUGE_CUSTOM_CLASSPATH]: classpath.toString().trim() }; 28 | } catch (e) { 29 | window.showErrorMessage(`Error calculating project classpath.\t\n${e.output.toString()}`); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /test/testdata/sampleProject/tests/step_implementation.js: -------------------------------------------------------------------------------- 1 | /* globals gauge*/ 2 | 3 | "use strict"; 4 | 5 | var assert = require("assert"); 6 | 7 | var vowels = ["a", "e", "i", "o", "u"]; 8 | 9 | var numberOfVowels = function (word) { 10 | var vowelArr = word.split("").filter(function (elem) { return vowels.indexOf(elem) > -1; }); 11 | return vowelArr.length; 12 | }; 13 | 14 | // -------------------------- 15 | // Gauge step implementations 16 | // -------------------------- 17 | 18 | step("Vowels in English language are .", function(vowelsGiven) { 19 | assert.equal(vowelsGiven, vowels.join("")); 20 | }); 21 | 22 | step("The word has vowels.", function(word, number) { 23 | assert.equal(number, numberOfVowels(word)); 24 | }); 25 | 26 | step("Almost all words have vowels ", function(table) { 27 | table.rows.forEach(function (row) { 28 | assert.equal(numberOfVowels(row.cells[0]), parseInt(row.cells[1])); 29 | }); 30 | }); 31 | 32 | // --------------- 33 | // Execution Hooks 34 | // --------------- 35 | 36 | beforeScenario(function () { 37 | assert.equal(vowels.join(""), "aeiou"); 38 | }); 39 | 40 | beforeScenario(function () { 41 | assert.equal(vowels[0], "a"); 42 | }, { tags: [ "single word" ]}); 43 | -------------------------------------------------------------------------------- /src/project/gradleProject.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { execSync } from 'child_process'; 4 | import { window } from 'vscode'; 5 | import { CLI } from '../cli'; 6 | import { GaugeProject } from './gaugeProject'; 7 | import { GAUGE_CUSTOM_CLASSPATH } from '../constants'; 8 | 9 | export class GradleProject extends GaugeProject { 10 | constructor(projectRoot: string, manifest: any) { 11 | super(projectRoot, manifest); 12 | } 13 | 14 | public getExecutionCommand(cli: CLI) { 15 | return cli.gradleCommand(); 16 | } 17 | 18 | public equals(o: Object): boolean { 19 | if (o == null) return false; 20 | if (!(o instanceof GradleProject)) return false; 21 | if (o === this) return true; 22 | return this.root() === (o as GradleProject).root(); 23 | } 24 | 25 | public envs(cli: CLI): NodeJS.ProcessEnv { 26 | try { 27 | let classpath = execSync(`${this.getExecutionCommand(cli)?.command} -q clean classpath`, {cwd: this.root()}); 28 | return {[GAUGE_CUSTOM_CLASSPATH]: classpath.toString().trim() }; 29 | } catch (e) { 30 | window.showErrorMessage(`Error calculating project classpath.\t\n${e.output.toString()}`); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /test/references.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { commands, Uri, window } from 'vscode'; 3 | import * as path from 'path'; 4 | 5 | let testDataPath = path.join(__dirname, '..', '..', 'test', 'testdata', 'sampleProject'); 6 | 7 | suite('Gauge References Tests', function () { 8 | this.timeout(10000); 9 | 10 | setup(async () => { 11 | await commands.executeCommand('workbench.action.closeAllEditors'); 12 | let implFile = Uri.file(path.join(testDataPath, 'tests', 'step_implementation.js')); 13 | await window.showTextDocument(implFile); 14 | }); 15 | 16 | test('should show references for step at cursor', async () => { 17 | await commands.executeCommand("workbench.action.focusFirstEditorGroup"); 18 | await commands.executeCommand("cursorMove", { to: 'down', by: 'line', value: 18 }); 19 | let value = commands.executeCommand('gauge.showReferences.atCursor'); 20 | assert.ok(value); 21 | }); 22 | 23 | test('should not show any reference if cursor is not in step context', async () => { 24 | await commands.executeCommand("workbench.action.focusFirstEditorGroup"); 25 | await commands.executeCommand("cursorMove", { to: 'down', by: 'line', value: 20 }); 26 | let value = commands.executeCommand('gauge.showReferences.atCursor'); 27 | assert.notEqual(value, true); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.tabSize": 4, 4 | "editor.insertSpaces": true, 5 | 6 | "files.eol": "\n", 7 | "files.trimTrailingWhitespace": true, 8 | 9 | "typescript.tsdk": "./node_modules/typescript/lib", 10 | "typescript.tsserver.trace": "off", 11 | "typescript.format.insertSpaceAfterCommaDelimiter": true, 12 | "typescript.format.insertSpaceAfterSemicolonInForStatements": true, 13 | "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": true, 14 | "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": true, 15 | "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, 16 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 17 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, 18 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, 19 | "typescript.format.placeOpenBraceOnNewLineForFunctions": false, 20 | "typescript.format.placeOpenBraceOnNewLineForControlBlocks": false, 21 | 22 | "javascript.validate.enable": false, 23 | 24 | "eslint.enable": true, 25 | "tslint.enable": true, 26 | "files.associations": { 27 | "*.spec": "gauge", 28 | "*.cpt": "gauge" 29 | }, 30 | "editor.formatOnSaveMode": "modificationsIfAvailable" 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "2.0.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // we run the custom script "compile" as defined in package.json 17 | "args": ["run", "compile", "--loglevel", "silent"], 18 | 19 | // The tsc compiler is started in watching mode 20 | "isBackground": true, 21 | 22 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 23 | "problemMatcher": "$tsc-watch", 24 | "tasks": [ 25 | { 26 | "label": "npm", 27 | "type": "shell", 28 | "command": "npm", 29 | "args": [ 30 | "run", 31 | "compile", 32 | "--loglevel", 33 | "silent" 34 | ], 35 | "isBackground": true, 36 | "problemMatcher": "$tsc-watch", 37 | "group": { 38 | "_id": "build", 39 | "isDefault": false 40 | } 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /resources/dark/play.svg: -------------------------------------------------------------------------------- 1 | 2 | Layer 1 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/light/play.svg: -------------------------------------------------------------------------------- 1 | 2 | Layer 1 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/welcomeNotifications.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { commands, ExtensionContext, Uri, window, workspace } from "vscode"; 4 | import { GAUGE_DOCS_URI, INSTALL_INSTRUCTION_URI } from "./constants"; 5 | 6 | const HAS_OPENED_BEFORE = "hasOpenedBefore"; 7 | const CONFIG_WELCOME_NOTIFICATION = 'gauge.welcomeNotification'; 8 | 9 | function shouldDisplayWelcomeNotification(isProjOpendBefore: boolean): boolean { 10 | let configValue = workspace 11 | .getConfiguration(CONFIG_WELCOME_NOTIFICATION).get('showOn'); 12 | if (configValue === 'never') return false; 13 | return !isProjOpendBefore; 14 | } 15 | 16 | export function showWelcomeNotification(context: ExtensionContext) { 17 | if (shouldDisplayWelcomeNotification(context.workspaceState.get(HAS_OPENED_BEFORE))) { 18 | window.showInformationMessage(`Gauge plugin initialised.`, 19 | "Learn more", "Do not show this again") 20 | .then((option) => { 21 | if (option === "Learn more") { 22 | commands.executeCommand('vscode.open', Uri.parse(GAUGE_DOCS_URI)); 23 | } else if (option === "Do not show this again") { 24 | workspace.getConfiguration().update(`${CONFIG_WELCOME_NOTIFICATION}.showOn`, 'never', true); 25 | } 26 | }); 27 | } 28 | context.workspaceState.update(HAS_OPENED_BEFORE, true); 29 | } 30 | 31 | export function showInstallGaugeNotification() { 32 | window.showErrorMessage( 33 | `Gauge executable not found! 34 | [Click here](${INSTALL_INSTRUCTION_URI}) for install instructions.` 35 | ); 36 | } -------------------------------------------------------------------------------- /src/execution/executionConfig.ts: -------------------------------------------------------------------------------- 1 | import { GaugeProject } from "../project/gaugeProject"; 2 | 3 | export class ExecutionConfig { 4 | public _inParallel: boolean = false; 5 | public _failed: boolean = false; 6 | public _repeat: boolean = false; 7 | public _debug: boolean = false; 8 | 9 | public _project: GaugeProject; 10 | public _status: string; 11 | 12 | public setParallel() { 13 | this._inParallel = true; 14 | return this; 15 | } 16 | 17 | public setProject(project: GaugeProject): ExecutionConfig { 18 | this._project = project; 19 | return this; 20 | } 21 | 22 | public setStatus(status: string): ExecutionConfig { 23 | this._status = status; 24 | return this; 25 | } 26 | 27 | public setDebug(): ExecutionConfig { 28 | this._debug = true; 29 | return this; 30 | } 31 | 32 | public setRepeat(): ExecutionConfig { 33 | this._repeat = true; 34 | return this; 35 | } 36 | 37 | public setFailed(): ExecutionConfig { 38 | this._failed = true; 39 | return this; 40 | } 41 | 42 | public getFailed(): boolean { 43 | return this._failed; 44 | } 45 | 46 | public getRepeat(): boolean { 47 | return this._repeat; 48 | } 49 | 50 | public getParallel(): boolean { 51 | return this._inParallel; 52 | } 53 | 54 | public getProject(): GaugeProject { 55 | return this._project; 56 | } 57 | 58 | public getStatus(): string { 59 | return this._status; 60 | } 61 | 62 | public getDebug(): boolean { 63 | return this._debug; 64 | } 65 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | 6 | module.exports = function(env, argv) { 7 | if (env === undefined) { 8 | env = {}; 9 | } 10 | 11 | const production = !!env.production; 12 | const minify = production; 13 | const sourceMaps = !env.production; 14 | 15 | const plugins = [ 16 | new webpack.optimize.ModuleConcatenationPlugin(), 17 | new TerserPlugin({ 18 | parallel: true, 19 | terserOptions: { 20 | ecma: 8, 21 | compress: minify ? {} : false, 22 | mangle: minify, 23 | output: { 24 | beautify: !minify, 25 | comments: false 26 | }, 27 | sourceMap: sourceMaps, 28 | } 29 | }) 30 | ]; 31 | 32 | return { 33 | entry: './src/extension.ts', 34 | target: 'node', 35 | output: { 36 | libraryTarget: 'commonjs2', 37 | filename: 'extension.js', 38 | path: path.resolve(__dirname, 'out') 39 | }, 40 | resolve: { 41 | extensions: ['.ts', '.js'] 42 | }, 43 | externals: [ 44 | { 45 | vscode: "commonjs vscode" 46 | }, 47 | ], 48 | devtool: sourceMaps ? 'source-map' : false, 49 | module: { 50 | rules: [ 51 | { 52 | test: /\.ts$/, 53 | use: [{ loader: 'ts-loader' }], 54 | exclude: /node_modules/ 55 | } 56 | ] 57 | }, 58 | plugins: plugins 59 | }; 60 | }; -------------------------------------------------------------------------------- /src/refactor/workspaceEditor.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | WorkspaceEdit, window, workspace, TextDocument, TextEdit, Uri, TextEditor, 5 | TextDocumentShowOptions, Position, Range 6 | } from "vscode"; 7 | import { dirname } from 'path'; 8 | import { mkdirpSync, existsSync, writeFileSync } from 'fs-extra'; 9 | export class WorkspaceEditor { 10 | private readonly _edit: WorkspaceEdit; 11 | 12 | constructor(edit: WorkspaceEdit) { 13 | this._edit = edit; 14 | } 15 | 16 | applyChanges() { 17 | this._edit.entries().forEach((tuple: [Uri, TextEdit[]]) => { 18 | this.applyTextEdit(tuple[0].fsPath, tuple[1]); 19 | }); 20 | } 21 | 22 | private writeInDocument(document: TextDocument, edit: TextEdit[]) { 23 | let lineNumberToFocus = edit[0].range.start.line; 24 | let options: TextDocumentShowOptions = { 25 | selection: new Range(new Position(lineNumberToFocus, 0), new Position(lineNumberToFocus, 0)) 26 | }; 27 | window.showTextDocument(document, options).then((editor: TextEditor) => { 28 | let newFileEdits = new WorkspaceEdit(); 29 | newFileEdits.set(document.uri, edit); 30 | workspace.applyEdit(newFileEdits); 31 | }); 32 | } 33 | 34 | private ensureDirectoryExistence(filePath: string) { 35 | let dir = dirname(filePath); 36 | if (!existsSync(dir)) { 37 | mkdirpSync(dir); 38 | } 39 | } 40 | 41 | private applyTextEdit(fileName: string, fileEdit: TextEdit[]): void { 42 | if (!existsSync(fileName)) { 43 | this.ensureDirectoryExistence(fileName); 44 | writeFileSync(fileName, "", {encoding: "utf8"}); 45 | } 46 | workspace.openTextDocument(fileName).then((document: TextDocument) => { 47 | this.writeInDocument(document, fileEdit); 48 | }); 49 | } 50 | } -------------------------------------------------------------------------------- /test/clients.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { GaugeClients } from '../src/gaugeClients'; 3 | import { GaugeProject } from '../src/project/gaugeProject'; 4 | import { tmpdir } from 'os'; 5 | import { join } from 'path'; 6 | import { LanguageClient } from 'vscode-languageclient/node'; 7 | suite('GaugeClients', () => { 8 | 9 | test('.get should give the project and client for given project root if exists', () => { 10 | let pr = join(tmpdir(), 'gauge'); 11 | let gaugeProject = new GaugeProject(pr, { langauge: 'java', plugins: {} }); 12 | let clients = new GaugeClients(); 13 | clients.set(pr, { project: gaugeProject, client: new LanguageClient("gauge", null, null) }); 14 | assert.equal(clients.get(pr).project, gaugeProject); 15 | }); 16 | 17 | test('.get should give the project and client for given file if blongs to a existing project', () => { 18 | let pr = join(tmpdir(), 'gauge'); 19 | let spec = join(pr, "specs", "example.spec"); 20 | let gaugeProject = new GaugeProject(pr, { langauge: 'java', plugins: {} }); 21 | let clients = new GaugeClients(); 22 | clients.set(pr, { project: gaugeProject, client: new LanguageClient("gauge", null, null) }); 23 | assert.ok(clients.get(pr)); 24 | assert.equal(clients.get(spec).project, gaugeProject); 25 | }); 26 | 27 | test('.get should give undefined is the given path does not belong to any project', () => { 28 | let tmp = tmpdir(); 29 | let pr = join(tmp, 'gauge'); 30 | let spec = join(tmp, "gauge2", "foo.spec"); 31 | let gaugeProject = new GaugeProject(pr, { langauge: 'java', plugins: {} }); 32 | let clients = new GaugeClients(); 33 | clients.set(pr, { project: gaugeProject, client: new LanguageClient("gauge", null, null) }); 34 | assert.ok(clients.get(spec) === undefined); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /src/project/gaugeProject.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { isAbsolute, relative } from 'path'; 4 | import { CLI, Command } from '../cli'; 5 | 6 | export class GaugeProject { 7 | private readonly _projectRoot: string; 8 | private readonly _isGaugeProject: boolean; 9 | private readonly _language: any; 10 | private readonly _plugins: any; 11 | 12 | public constructor(projectRoot: string, manifest: any) { 13 | this._projectRoot = projectRoot; 14 | this._isGaugeProject = manifest != null; 15 | if (this._isGaugeProject) { 16 | this._language = manifest.Language; 17 | this._plugins = manifest.Plugins; 18 | } 19 | } 20 | 21 | public getExecutionCommand(cli: CLI): Command { 22 | return cli.gaugeCommand(); 23 | } 24 | 25 | public isGaugeProject(): boolean { 26 | return this._isGaugeProject; 27 | } 28 | 29 | public language(): string { 30 | return this._language; 31 | } 32 | 33 | public hasFile(file: string) { 34 | if (this.root() === file) return true; 35 | const rel = relative(this.root(), file); 36 | return !rel.startsWith('..') && !isAbsolute(rel); 37 | } 38 | 39 | public isProjectLanguage(language: string): any { 40 | return this._language === language; 41 | } 42 | 43 | public root(): any { 44 | return this._projectRoot; 45 | } 46 | 47 | public toString(): string { 48 | return `Project Path: ${this._projectRoot}\n` + 49 | `Language: ${this._language}\n` + 50 | `Plugins:${this._plugins.join(', ')}`; 51 | } 52 | 53 | public equals(o: Object): boolean { 54 | if (o == null) return false; 55 | if (!(o instanceof GaugeProject)) return false; 56 | if (o === this) return true; 57 | return this.root() === (o as GaugeProject).root(); 58 | } 59 | 60 | public envs(cli: CLI): NodeJS.ProcessEnv { 61 | return {}; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/githubRelease.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: deployment 4 | 5 | jobs: 6 | github-release: 7 | if: github.event.deployment.environment == 'production' 8 | name: Deploy to github 9 | runs-on: ubuntu-latest 10 | env: 11 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 12 | steps: 13 | - uses: actions/checkout@v6 14 | 15 | - name: Set up NodeJS 16 | uses: actions/setup-node@v6 17 | with: 18 | node-version: 24 19 | 20 | - name: Build artifacts 21 | run: | 22 | npm run build 23 | 24 | - name: Release on github 25 | run: | 26 | if [ -z "$version" ]; then 27 | version=$(ls gauge-* | head -1 | sed "s/\.[^\.]*$//" | sed "s/gauge-//" | sed "s/-[a-z]*\.[a-z0-9_]*$//"); 28 | fi 29 | 30 | echo "---------------------------" 31 | echo "Updating release v$version" 32 | echo "---------------------------" 33 | 34 | echo -e "gauge-vscode $version\n\n" > desc.txt 35 | 36 | release_description=$(ruby -e "$(curl -sSfL https://github.com/getgauge/gauge/raw/master/build/create_release_text.rb)" getgauge gauge-vscode) 37 | 38 | echo "$release_description" >> desc.txt 39 | gh release create --title "gauge-vscode v${version}" --notes-file ./desc.txt "v${version}" *.vsix 40 | 41 | marketplace-release: 42 | name: Deploy to marketplace 43 | runs-on: ubuntu-latest 44 | needs: [github-release] 45 | env: 46 | VS_PAT: '${{ secrets.VS_PAT }}' 47 | steps: 48 | - uses: actions/checkout@v6 49 | 50 | - name: Set up NodeJS 51 | uses: actions/setup-node@v6 52 | with: 53 | node-version: 24 54 | 55 | - name: Build artifacts 56 | run: | 57 | npm run build 58 | 59 | - name: Upload to marketplace 60 | run: | 61 | version=$(ls gauge-* | head -1 | sed "s/\.[^\.]*$//" | sed "s/gauge-//" | sed "s/-[a-z]*\.[a-z0-9_]*$//"); 62 | npm install 63 | npm run publish -- --packagePath gauge-$version.vsix -p $VS_PAT 64 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@vscode/test-electron'; 2 | import * as path from 'path'; 3 | import { engines } from "../package.json"; 4 | 5 | async function go(version?: string) { 6 | try { 7 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 8 | const extensionTestsPath = path.resolve(__dirname, './'); 9 | const testWorkspace = path.resolve(__dirname, '../../test/testdata/sampleProject'); 10 | 11 | if (version===undefined){ 12 | // run tests with current stable vscode version 13 | version = "stable"; 14 | } 15 | console.log(`Running tests with version: ${version}`) 16 | 17 | await runTests({ 18 | extensionDevelopmentPath, 19 | extensionTestsPath, 20 | launchArgs: [testWorkspace, '--disable-extensions'], 21 | version: version 22 | }); 23 | 24 | } catch (err) { 25 | console.error('---Failed to run tests---'); 26 | console.error(err); 27 | process.exit(1); 28 | } 29 | } 30 | 31 | console.log(process.argv); 32 | 33 | if (process.argv[2] && process.argv[2] === '--compatibility') { 34 | // Running the tests with the minimum vscode version that this plugin supports. 35 | // The version is directly taken from this plugins package version 36 | // 37 | // This rerun is here to catch incompatibilities of @types/vscode with the lsp 38 | // packages vscode-languageclient 39 | 40 | // Upgrading vscode-languageclient might require a @types/vscode version bump 41 | // (these version bumps are not mentioned in their release notes). 42 | // Bumping @types/vscode will require bumping the minimum supported vscode version in 43 | // package.json (engines["vscode"]) as well. 44 | // The value of engines["vscode"] cannot be a pinned version "~1.67.0" but must use the 45 | // minimum version notation "^1.67.0" indicating that this is the minimum supported 46 | // vscode version 47 | const version = engines["vscode"].replace("^", ""); 48 | go(version) 49 | } else { 50 | go() 51 | } 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/vscode.yml: -------------------------------------------------------------------------------- 1 | name: vscode 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [macos-latest, ubuntu-latest, windows-2022] 19 | steps: 20 | - uses: actions/checkout@v6 21 | 22 | - name: Set up NodeJS 23 | uses: actions/setup-node@v6 24 | with: 25 | node-version: 24 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@v6 29 | with: 30 | check-latest: true 31 | go-version: '1.25' 32 | 33 | - name: build 34 | run: | 35 | npm run build 36 | 37 | - uses: getgauge/setup-gauge@master 38 | with: 39 | gauge-version: master 40 | gauge-plugins: screenshot,html-report,js 41 | 42 | - name: Run tests (linux) 43 | if: matrix.os == 'ubuntu-latest' 44 | run: | 45 | xvfb-run --auto-servernum npm test 46 | 47 | - name: Run compatibility tests (linux) 48 | if: matrix.os == 'ubuntu-latest' 49 | run: | 50 | xvfb-run --auto-servernum npm run compatibilityTest 51 | 52 | - name: Run tests (macos) 53 | if: matrix.os == 'macos-latest' 54 | run: | 55 | npm test 56 | 57 | - name: Run compatibility tests (macos) 58 | if: matrix.os == 'macos-latest' 59 | run: | 60 | npm run compatibilityTest 61 | 62 | - name: Run tests (windows) 63 | if: matrix.os == 'windows-2022' 64 | shell: pwsh 65 | run: | 66 | npm test 67 | 68 | - name: Run compatibility tests (windows) 69 | if: matrix.os == 'windows-2022' 70 | shell: pwsh 71 | run: | 72 | npm run compatibilityTest 73 | 74 | - name: Upload logs 75 | uses: actions/upload-artifact@v5 76 | if: failure() 77 | with: 78 | name: logs-${{ matrix.os }} 79 | path: test/testdata/sampleProject/ 80 | -------------------------------------------------------------------------------- /src/project/projectFactory.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from "fs"; 2 | import { join, parse } from "path"; 3 | import { GAUGE_MANIFEST_FILE, MAVEN_POM, GRADLE_BUILD } from "../constants"; 4 | import { GaugeProject } from "./gaugeProject"; 5 | import { MavenProject } from "./mavenProject"; 6 | import { GradleProject } from "./gradleProject"; 7 | 8 | export class ProjectFactory { 9 | public static javaProjectBuilders: Array<{ 10 | predicate: (root: string) => boolean, 11 | build: (root: string, data: string) => GaugeProject 12 | }> = [ 13 | { 14 | predicate: (root: string) => existsSync(join(root, MAVEN_POM)), 15 | build: (root: string, data: any) => new MavenProject(root, data) 16 | }, 17 | { 18 | predicate: (root: string) => existsSync(join(root, GRADLE_BUILD)), 19 | build: (root: string, data: any) => new GradleProject(root, data) 20 | } 21 | ]; 22 | 23 | public static get(path: string): GaugeProject { 24 | if (!path) throw new Error(`${path} does not belong to a valid gauge project.`); 25 | const content = readFileSync(join(path, GAUGE_MANIFEST_FILE)); 26 | const data = JSON.parse(content.toString()); 27 | if (data.Language && data.Language === "java") { 28 | for (const builder of this.javaProjectBuilders) { 29 | if (builder.predicate(path)) return builder.build(path, data); 30 | } 31 | } 32 | return new GaugeProject(path, data); 33 | } 34 | 35 | public static isGaugeProject(dir: string): boolean { 36 | return existsSync(join(dir, GAUGE_MANIFEST_FILE)); 37 | } 38 | 39 | public static getGaugeRootFromFilePath(filepath: string): string { 40 | while (!this.isGaugeProject(filepath)) { 41 | filepath = parse(filepath).dir; 42 | } 43 | return filepath; 44 | } 45 | public static getProjectByFilepath(fsPath: string): GaugeProject { 46 | let projectRoot = this.getGaugeRootFromFilePath(fsPath); 47 | return this.get(projectRoot); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/gaugeWorkspace.proposed.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { workspace, Disposable } from 'vscode'; 4 | import { DynamicFeature, RegistrationData, BaseLanguageClient, FeatureState, ClientCapabilities, InitializedParams, RequestType0 } from 'vscode-languageclient'; 5 | 6 | import { SaveFilesRequest, GaugeClientCapabilities } from './protocol/gauge.proposed'; 7 | 8 | export class GaugeWorkspaceFeature implements DynamicFeature { 9 | 10 | private _listeners: Map = new Map(); 11 | 12 | constructor(private _client: BaseLanguageClient) { 13 | } 14 | 15 | public registrationType; 16 | 17 | public get messages(): RequestType0 { 18 | return SaveFilesRequest.type; 19 | } 20 | 21 | public fillInitializeParams(params: InitializedParams): void { 22 | } 23 | 24 | public fillClientCapabilities(capabilities: ClientCapabilities): void { 25 | let workspaceCapabilities = capabilities as GaugeClientCapabilities; 26 | workspaceCapabilities.saveFiles = true; 27 | } 28 | 29 | public initialize(): void { 30 | let client = this._client; 31 | client.onRequest(SaveFilesRequest.type.method, () => { 32 | return workspace.saveAll(false).then(() => { 33 | return null; 34 | }); 35 | }); 36 | } 37 | 38 | public register(data: RegistrationData): void { 39 | } 40 | 41 | public unregister(id: string): void { 42 | let disposable = this._listeners.get(id); 43 | if (disposable === void 0) { 44 | return; 45 | } 46 | this._listeners.delete(id); 47 | disposable.dispose(); 48 | } 49 | 50 | public dispose(): void { 51 | for (let disposable of this._listeners.values()) { 52 | disposable.dispose(); 53 | } 54 | this._listeners.clear(); 55 | } 56 | 57 | public clear():void{} 58 | 59 | public getState(): FeatureState { 60 | return { 61 | kind: "workspace", 62 | id: this.registrationType, 63 | registrations: this._listeners.size > 0, 64 | }; 65 | } 66 | } -------------------------------------------------------------------------------- /test/config/gaugeConfig.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { homedir } from 'os'; 3 | import { join } from 'path'; 4 | import GaugeConfig from '../../src/config/gaugeConfig'; 5 | let appDataEnv; 6 | suite('GaugeConfig', () => { 7 | setup( () => { 8 | appDataEnv = process.env.APPDATA; 9 | }); 10 | teardown( () => { 11 | delete process.env.GAUGE_HOME; 12 | process.env.APPDATA = appDataEnv; 13 | }); 14 | 15 | test('should calculate plugins path for window platform', (done) => { 16 | let appData = '/Users/userName/AppData/Roaming'; 17 | process.env.APPDATA = appData; 18 | const expectedPluginsPath = join(appData, 'Gauge', 'plugins'); 19 | 20 | const actualPluginsPath = new GaugeConfig('win32').pluginsPath(); 21 | assert.equal(actualPluginsPath, expectedPluginsPath); 22 | done(); 23 | }); 24 | 25 | test('should calculate plugins path for window platform when GAUGE_HOME env is set', (done) => { 26 | let gaugeHome = '/Users/userName/gaugeHome'; 27 | process.env.GAUGE_HOME = gaugeHome; 28 | const expectedPluginsPath = join(gaugeHome, 'plugins'); 29 | 30 | const actualPluginsPath = new GaugeConfig('win32').pluginsPath(); 31 | assert.equal(actualPluginsPath, expectedPluginsPath); 32 | done(); 33 | }); 34 | 35 | test('should calculate plugins path for non window platform', (done) => { 36 | const expectedPluginsPath = join(homedir(), '.gauge', 'plugins'); 37 | 38 | const actualPluginsPath = new GaugeConfig('darwin').pluginsPath(); 39 | assert.equal(actualPluginsPath, expectedPluginsPath); 40 | done(); 41 | }); 42 | 43 | test('should calculate plugins path for non window platform when GAUGE_HOME env is set', (done) => { 44 | let gaugeHome = '/Users/userName/gaugeHome'; 45 | process.env.GAUGE_HOME = gaugeHome; 46 | const expectedPluginsPath = join(gaugeHome, 'plugins'); 47 | 48 | const actualPluginsPath = new GaugeConfig('darwin').pluginsPath(); 49 | assert.equal(actualPluginsPath, expectedPluginsPath); 50 | done(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/execution/lineProcessors.ts: -------------------------------------------------------------------------------- 1 | import { GaugeWorkspace } from "../gaugeWorkspace"; 2 | import { GaugeExecutor } from "./gaugeExecutor"; 3 | import { GaugeDebugger } from "./debug"; 4 | import { window } from "vscode"; 5 | 6 | export interface LineTextProcessor { 7 | canProcess(lineText: string): boolean; 8 | process(lineText: string, gaugeDebugger: GaugeDebugger): void; 9 | } 10 | 11 | abstract class BaseProcessor implements LineTextProcessor { 12 | protected eventPrefix: string; 13 | 14 | constructor(prefix: string) { 15 | this.eventPrefix = prefix; 16 | } 17 | 18 | public canProcess(lineText: string): boolean { 19 | return lineText.includes(this.eventPrefix); 20 | } 21 | 22 | abstract process(lineText: string, gaugeDebugger: GaugeDebugger): void; 23 | } 24 | 25 | export class ReportEventProcessor extends BaseProcessor { 26 | 27 | constructor(private workspace: GaugeWorkspace) { 28 | super("Successfully generated html-report to => "); 29 | } 30 | 31 | public process(lineText: string): void { 32 | if (!this.canProcess(lineText)) return; 33 | let reportPath = lineText.replace(this.eventPrefix, ""); 34 | this.workspace.setReportPath(reportPath); 35 | } 36 | } 37 | 38 | export class DebuggerAttachedEventProcessor extends BaseProcessor { 39 | 40 | constructor(private executor: GaugeExecutor) { 41 | super("Runner Ready for Debugging"); 42 | } 43 | 44 | public process(lineText: string, gaugeDebugger: GaugeDebugger): void { 45 | if (!this.canProcess(lineText)) return; 46 | gaugeDebugger.addProcessId(+lineText.replace(/^\D+/g, '')); 47 | gaugeDebugger.startDebugger().catch(error => { 48 | window.showErrorMessage(`Failed to start debugger: ${error.message}`); 49 | this.executor.cancel(false); 50 | }); 51 | } 52 | } 53 | 54 | export class DebuggerNotAttachedEventProcessor extends BaseProcessor { 55 | 56 | constructor(private executor: GaugeExecutor) { 57 | super("No debugger attached"); 58 | } 59 | 60 | public process(lineText: string): void { 61 | if (!this.canProcess(lineText)) return; 62 | window.showErrorMessage("No debugger attached. Stopping the execution"); 63 | this.executor.cancel(false); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /snippets/gauge.json: -------------------------------------------------------------------------------- 1 | { 2 | ".source.gauge": { 3 | "specification": { 4 | "prefix": "spec", 5 | "body": [ 6 | "# ${1:SPECIFICATION_HEADING}", 7 | "$0" 8 | ], 9 | "description" : "New Specification" 10 | }, 11 | "scenario": { 12 | "prefix": "sce", 13 | "body": [ 14 | "## ${1:Scenario Heading}", 15 | "* $0" 16 | ], 17 | "description" : "New Scenario" 18 | }, 19 | "concept": { 20 | "prefix": "cpt", 21 | "body": [ 22 | "# ${1:Concept Heading}", 23 | "* $0" 24 | ], 25 | "description" : "New Concept" 26 | }, 27 | "table_1": { 28 | "prefix": "table:1", 29 | "body": [ 30 | "|${1:HEADER}|", 31 | "|------|", 32 | "|${2:value}|", 33 | "|${3:value}$0|" 34 | ], 35 | "description" : "table with 1 column" 36 | }, 37 | "table_2": { 38 | "prefix": "table:2", 39 | "body": [ 40 | "|${1:HEADER}|${2:HEADER}|", 41 | "|------|------|", 42 | "|${3:value}|${4:value}|", 43 | "|${5:value}|${6:value}$0|" 44 | ], 45 | "description" : "table with 2 columns" 46 | }, 47 | "table_3": { 48 | "prefix": "table:3", 49 | "body": [ 50 | "|${1:HEADER}|${2:HEADER}|${3:HEADER}|", 51 | "|------|------|------|", 52 | "|${4:value}|${5:value}|${6:value}|", 53 | "|${7:value}|${8:value}|${9:value}$0|" 54 | ], 55 | "description" : "table with 3 columns" 56 | }, 57 | "table_4": { 58 | "prefix": "table:4", 59 | "body": [ 60 | "|${1:HEADER}|${2:HEADER}|${3:HEADER}|${4:HEADER}|", 61 | "|------|------|------|------|", 62 | "|${5:value}|${6:value}|${7:value}|${8:value}|", 63 | "|${9:value}|${10:value}|${11:value}|${12:value}$0|" 64 | ], 65 | "description" : "table with 4 columns" 66 | }, 67 | "table_5": { 68 | "prefix": "table:5", 69 | "body": [ 70 | "|${1:HEADER}|${2:HEADER}|${3:HEADER}|${4:HEADER}|${5:HEADER}|", 71 | "|------|------|------|------|------|", 72 | "|${6:value}|${7:value}|${8:value}|${9:value}|${10:value}|", 73 | "|${11:value}|${12:value}|${13:value}|${14:value}|${15:value}$0|" 74 | ], 75 | "description" : "table with 5 columns" 76 | }, 77 | "table_6": { 78 | "prefix": "table:6", 79 | "body": [ 80 | "|${1:HEADER}|${2:HEADER}|${3:HEADER}|${4:HEADER}|${5:HEADER}|${6:HEADER}|", 81 | "|------|------|------|------|------|------|", 82 | "|${7:value}|${8:value}|${9:value}|${10:value}|${11:value}|${12:value}|", 83 | "|${13:value}|${14:value}|${15:value}|${16:value}|${17:value}|${18:value}$0|" 84 | ], 85 | "description" : "table with 6 columns" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/execution/outputChannel.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { LineBuffer } from './lineBuffer'; 5 | import * as cp from 'child_process'; 6 | import * as path from 'path'; 7 | 8 | export class OutputChannel { 9 | private process: cp.ChildProcess; 10 | private outBuf: LineBuffer; 11 | private errBuf: LineBuffer; 12 | private chan: vscode.OutputChannel; 13 | private projectRoot: string; 14 | 15 | constructor(outputChannel, initial: string, projectRoot: string) { 16 | this.projectRoot = projectRoot; 17 | this.outBuf = new LineBuffer(); 18 | this.errBuf = new LineBuffer(); 19 | this.chan = outputChannel; 20 | this.chan.clear(); 21 | this.chan.appendLine(initial); 22 | this.setup(); 23 | } 24 | 25 | private setup() { 26 | this.chan.show(true); 27 | this.outBuf.onLine((line) => this.chan.appendLine(line)); 28 | this.outBuf.onDone((last) => last && this.chan.appendLine(last)); 29 | this.errBuf.onLine((line) => this.chan.appendLine(line)); 30 | this.errBuf.onDone((last) => last && this.chan.appendLine(last)); 31 | } 32 | 33 | public appendOutBuf(line: string) { 34 | let regexes: RegExp[] = [/Specification: /, /at Object.* \(/]; 35 | let lineArray = line.split("\n"); 36 | for (let j = 0; j < lineArray.length; j++) { 37 | for (let i = 0; i < regexes.length; i++) { 38 | let matches = lineArray[j].match(regexes[i]); 39 | if (matches && !lineArray[j].includes(this.projectRoot)) { 40 | lineArray[j] = lineArray[j].replace(matches[0], matches[0] + this.projectRoot + path.sep); 41 | } 42 | } 43 | } 44 | line = lineArray.join("\n"); 45 | this.outBuf.append(line); 46 | } 47 | 48 | public appendErrBuf(line: string) { 49 | this.errBuf.append(line); 50 | } 51 | 52 | public onFinish(resolve: (value?: boolean | PromiseLike) => void, 53 | code: number, successMessage: string, failureMessage: string, aborted?: boolean) { 54 | this.outBuf.done(); 55 | this.errBuf.done(); 56 | 57 | if (aborted) { 58 | this.chan.appendLine('Run stopped by user.'); 59 | resolve(false); 60 | return; 61 | } 62 | 63 | if (code) { 64 | this.chan.appendLine(failureMessage); 65 | } else { 66 | this.chan.appendLine(successMessage); 67 | } 68 | resolve(code === 0); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/gaugeReference.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Disposable, commands, workspace, Uri, CancellationTokenSource, window } from "vscode"; 4 | import { GaugeVSCodeCommands, VSCodeCommands } from "./constants"; 5 | import { 6 | LanguageClient, TextDocumentIdentifier, Location as LSLocation, Position as LSPosition 7 | } from 'vscode-languageclient/node'; 8 | import { GaugeClients } from "./gaugeClients"; 9 | 10 | export class ReferenceProvider extends Disposable { 11 | private _disposable: Disposable; 12 | constructor(private clients: GaugeClients) { 13 | super(() => this.dispose()); 14 | 15 | this._disposable = Disposable.from( 16 | commands.registerCommand( 17 | GaugeVSCodeCommands.ShowReferencesAtCursor, this.showStepReferencesAtCursor(clients)), 18 | commands.registerCommand( 19 | GaugeVSCodeCommands.ShowReferences, this.showStepReferences(clients)) 20 | ); 21 | } 22 | 23 | private showStepReferences(clients: GaugeClients): 24 | (uri: string, position: LSPosition, stepValue: string) => Thenable { 25 | return (uri: string, position: LSPosition, stepValue: string) => { 26 | let languageClient = clients.get(Uri.parse(uri).fsPath).client; 27 | return languageClient.sendRequest("gauge/stepReferences", stepValue, new CancellationTokenSource().token) 28 | .then( 29 | (locations: LSLocation[]) => { 30 | return this.showReferences(locations, uri, languageClient, position); 31 | }); 32 | }; 33 | } 34 | 35 | private showStepReferencesAtCursor(clients: GaugeClients): () => Thenable { 36 | return (): Thenable => { 37 | let position = window.activeTextEditor.selection.active; 38 | let documentId = TextDocumentIdentifier.create(window.activeTextEditor.document.uri.toString()); 39 | let activeEditor = window.activeTextEditor.document.uri; 40 | let languageClient = clients.get(activeEditor.fsPath).client; 41 | let params = { textDocument: documentId, position }; 42 | return languageClient.sendRequest("gauge/stepValueAt", params, new CancellationTokenSource().token).then( 43 | (stepValue: string) => { 44 | return this.showStepReferences(clients)(documentId.uri, position, stepValue); 45 | }); 46 | }; 47 | } 48 | 49 | private showReferences(locations: LSLocation[], uri: string, languageClient: LanguageClient, position: LSPosition): 50 | Thenable { 51 | if (locations) { 52 | return commands.executeCommand(VSCodeCommands.ShowReferences, Uri.parse(uri), 53 | languageClient.protocol2CodeConverter.asPosition(position), 54 | locations.map(languageClient.protocol2CodeConverter.asLocation)); 55 | } 56 | window.showInformationMessage('Action NA: Try this on an implementation.'); 57 | return Promise.resolve(false); 58 | } 59 | } -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { commands } from 'vscode'; 4 | 5 | export enum VSCodeCommands { 6 | Open = 'vscode.open', 7 | SetContext = 'setContext', 8 | ShowReferences = 'editor.action.showReferences', 9 | Preview = 'vscode.previewHtml', 10 | OpenFolder = 'vscode.openFolder', 11 | ReloadWindow = 'workbench.action.reloadWindow' 12 | } 13 | 14 | export enum GaugeVSCodeCommands { 15 | SaveRecommendedSettings = 'gauge.config.saveRecommended', 16 | ShowReport = 'gauge.report.html', 17 | StopExecution = 'gauge.stopExecution', 18 | Execute = 'gauge.execute', 19 | Debug = 'gauge.debug', 20 | ExecuteInParallel = 'gauge.execute.inParallel', 21 | ExecuteFailed = 'gauge.execute.failed', 22 | ExecuteSpec = 'gauge.execute.specification', 23 | ExecuteAllSpecs = 'gauge.execute.specification.all', 24 | ExecuteAllSpecExplorer = 'gauge.specexplorer.runAllActiveProjectSpecs', 25 | ExecuteNode = 'gauge.specexplorer.runNode', 26 | DebugNode = 'gauge.specexplorer.debugNode', 27 | ExecuteScenario = 'gauge.execute.scenario', 28 | ExecuteScenarios = 'gauge.execute.scenarios', 29 | GenerateStepStub = 'gauge.generate.step', 30 | GenerateConceptStub = 'gauge.generate.concept', 31 | ShowReferences = 'gauge.showReferences', 32 | ShowReferencesAtCursor = 'gauge.showReferences.atCursor', 33 | Open = 'gauge.open', 34 | RepeatExecution = 'gauge.execute.repeat', 35 | SwitchProject = 'gauge.specexplorer.switchProject', 36 | ExtractConcept = 'gauge.extract.concept', 37 | ExecuteInTerminal = "gauge.executeIn.terminal", 38 | CreateProject = "gauge.createProject", 39 | CreateSpecification = "gauge.create.specification", 40 | } 41 | 42 | export enum GaugeCommands { 43 | Gauge = 'gauge', 44 | Version = '--version', 45 | MachineReadable = '--machine-readable', 46 | Run = 'run', 47 | Init = 'init', 48 | Install = "install", 49 | } 50 | 51 | export enum GaugeCommandContext { 52 | Enabled = 'gauge:enabled', 53 | Activated = 'gauge:activated', 54 | GaugeSpecExplorer = 'gauge:specExplorer', 55 | MultiProject = 'gauge:multipleProjects?', 56 | } 57 | 58 | export function setCommandContext(key: GaugeCommandContext | string, value: any) { 59 | return commands.executeCommand(VSCodeCommands.SetContext, key, value); 60 | } 61 | 62 | export enum GaugeRequests { 63 | Specs = 'gauge/specs', 64 | Scenarios = 'gauge/scenarios', 65 | Files = "gauge/getImplFiles", 66 | AddStub = 'gauge/putStubImpl', 67 | GenerateConcept = 'gauge/generateConcept', 68 | SpecDirs = 'gauge/specDirs' 69 | } 70 | 71 | export enum GaugeRunners { 72 | Java = "java", 73 | Dotnet = "dotnet" 74 | } 75 | 76 | export const LAST_REPORT_PATH = 'gauge.execution.report'; 77 | export const COPY_TO_CLIPBOARD = 'Copy To Clipboard'; 78 | export const NEW_FILE = 'New File'; 79 | export const GAUGE_TEMPLATE_URL = 'https://downloads.gauge.org/templates'; 80 | export const GAUGE_MANIFEST_FILE = 'manifest.json'; 81 | export const MAVEN_POM = "pom.xml"; 82 | export const MAVEN_COMMAND = "mvn"; 83 | export const GRADLE_COMMAND = "gradlew"; 84 | export const GRADLE_BUILD = 'build.gradle'; 85 | export const GAUGE_CUSTOM_CLASSPATH = 'gauge_custom_classpath'; 86 | export const GAUGE_DOCS_URI = 'https://docs.gauge.org'; 87 | export const INSTALL_INSTRUCTION_URI = `${GAUGE_DOCS_URI}/getting_started/installing-gauge.html`; -------------------------------------------------------------------------------- /test/lineProcessors.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from 'assert'; 3 | import { instance, mock, verify, anyString, when } from 'ts-mockito'; 4 | import { ReportEventProcessor, DebuggerAttachedEventProcessor } from '../src/execution/lineProcessors'; 5 | import { GaugeExecutor } from '../src/execution/gaugeExecutor'; 6 | import { GaugeDebugger } from '../src/execution/debug'; 7 | import { GaugeWorkspace } from '../src/gaugeWorkspace'; 8 | 9 | suite('ReportEventProcessor', () => { 10 | suite('.process', () => { 11 | test('should process a given line text and set report path', () => { 12 | let workspace: GaugeWorkspace = mock(GaugeWorkspace); 13 | let processor = new ReportEventProcessor(instance(workspace)); 14 | let lineText = "Successfully generated html-report to => path"; 15 | assert.ok(processor.canProcess(lineText)); 16 | processor.process(lineText); 17 | verify(workspace.setReportPath("path")).called(); 18 | }); 19 | 20 | test('should not process if line text does not contain the prefix', () => { 21 | let workspace: GaugeWorkspace = mock(GaugeWorkspace); 22 | let processor = new ReportEventProcessor(instance(workspace)); 23 | let lineText = "some other stdout event"; 24 | processor.process(lineText); 25 | verify(workspace.setReportPath(anyString())).never(); 26 | }); 27 | }); 28 | }); 29 | 30 | suite('DebuggerAttachedEventProcessor', () => { 31 | suite('.process', () => { 32 | test('should process a given line text and set the process ID', () => { 33 | let executor: GaugeExecutor = mock(GaugeExecutor); 34 | let gaugeDebugger = mock(GaugeDebugger); 35 | when(gaugeDebugger.startDebugger()).thenReturn(Promise.resolve(true)); 36 | let processor = new DebuggerAttachedEventProcessor(instance(executor)); 37 | let lineText = "Runner Ready for Debugging at Process ID 23456"; 38 | assert.ok(processor.canProcess(lineText)); 39 | processor.process(lineText, instance(gaugeDebugger)); 40 | verify(gaugeDebugger.addProcessId(23456)).called(); 41 | verify(gaugeDebugger.startDebugger()).called(); 42 | }); 43 | 44 | test('should process a given line text and start debugger', () => { 45 | let executor: GaugeExecutor = mock(GaugeExecutor); 46 | let gaugeDebugger = mock(GaugeDebugger); 47 | when(gaugeDebugger.startDebugger()).thenReturn(Promise.resolve(true)); 48 | let processor = new DebuggerAttachedEventProcessor(instance(executor)); 49 | let lineText = "Runner Ready for Debugging"; 50 | assert.ok(processor.canProcess(lineText)); 51 | processor.process(lineText, instance(gaugeDebugger)); 52 | verify(gaugeDebugger.startDebugger()).called(); 53 | }); 54 | 55 | test('should not process if line text does not contain the prefix', () => { 56 | let executor: GaugeExecutor = mock(GaugeExecutor); 57 | let gaugeDebugger = mock(GaugeDebugger); 58 | let processor = new DebuggerAttachedEventProcessor(instance(executor)); 59 | let lineText = "some other event"; 60 | processor.process(lineText, instance(gaugeDebugger)); 61 | verify(gaugeDebugger.startDebugger()).never(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /resources/dark/number.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/number.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/dark/string.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/light/string.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at gauge-coc@thoughtworks.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /test/execution/outputChannel.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as path from 'path'; 3 | import { OutputChannel } from '../../src/execution/outputChannel'; 4 | import * as vscode from 'vscode'; 5 | 6 | suite('Output Channel', () => { 7 | class MockVSOutputChannel implements vscode.OutputChannel { 8 | replace(value: string): void {} 9 | name: string; 10 | text: string; 11 | append(value: string): void { } 12 | appendLine(value: string): void { 13 | this.text = value; 14 | } 15 | clear(): void { 16 | this.text = ''; 17 | } 18 | show(preserveFocus?: boolean): void; 19 | show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; 20 | show(column?: any, preserveFocus?: any) { } 21 | hide(): void { } 22 | dispose(): void { } 23 | 24 | read(): string { 25 | return this.text; 26 | } 27 | } 28 | 29 | test('should convert specification path from relative to absolute', (done) => { 30 | let chan = new MockVSOutputChannel(); 31 | let outputChannel = new OutputChannel(chan, "", "project"); 32 | outputChannel.appendOutBuf(" Specification: " + path.join("specs", "example.spec:19") + "\n"); 33 | 34 | assert.equal(chan.read(), " Specification: " + path.join("project", "specs", "example.spec:19")); 35 | done(); 36 | }); 37 | 38 | test('should convert implementation path from relative to absolute', (done) => { 39 | let chan = new MockVSOutputChannel(); 40 | let outputChannel = new OutputChannel(chan, "", "project"); 41 | outputChannel.appendOutBuf(" at Object. (" + 42 | path.join("tests", "step_implementation.js:24:10") + ")\n"); 43 | 44 | assert.equal(chan.read(), " at Object. (" + 45 | path.join("project", "tests", "step_implementation.js:24:10)")); 46 | done(); 47 | }); 48 | 49 | test('should not change implementation path from relative to absolute if it is already absolute', (done) => { 50 | let chan = new MockVSOutputChannel(); 51 | let outputChannel = new OutputChannel(chan, "", "projectRoot"); 52 | outputChannel.appendOutBuf(" at Object. (" + 53 | path.join("projectRoot", "tests", "step_implementation.js:24:10") + ")\n"); 54 | 55 | assert.equal(chan.read(), " at Object. (" + 56 | path.join("projectRoot", "tests", "step_implementation.js:24:10)")); 57 | done(); 58 | }); 59 | 60 | test('should convert implementation path from relative to absolute for lamda methods', (done) => { 61 | let chan = new MockVSOutputChannel(); 62 | let outputChannel = new OutputChannel(chan, "", "project"); 63 | outputChannel.appendOutBuf(" at Object. (" + 64 | path.join("tests", "step_implementation.js:24:10") + ")\n"); 65 | 66 | assert.equal(chan.read(), " at Object. (" + 67 | path.join("project", "tests", "step_implementation.js:24:10)")); 68 | done(); 69 | }); 70 | 71 | test('should convert implementation path from relative to absolute for lamda methods in hooks', (done) => { 72 | let chan = new MockVSOutputChannel(); 73 | let outputChannel = new OutputChannel(chan, "", "project"); 74 | outputChannel.appendOutBuf(" at Object. (" + 75 | path.join("tests", "step_implementation.js:24:10") + ")\n"); 76 | 77 | assert.equal(chan.read(), " at Object. (" + 78 | path.join("project", "tests", "step_implementation.js:24:10)")); 79 | done(); 80 | }); 81 | }); -------------------------------------------------------------------------------- /test/project.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { tmpdir } from 'os'; 3 | import { createSandbox } from 'sinon'; 4 | import * as child_process from 'child_process'; 5 | import { join } from 'path'; 6 | import { GaugeProject } from '../src/project/gaugeProject'; 7 | import { MavenProject } from '../src/project/mavenProject'; 8 | import { CLI } from '../src/cli'; 9 | import { window } from 'vscode'; 10 | import { GradleProject } from '../src/project/gradleProject'; 11 | 12 | suite('GaugeProject', () => { 13 | suite('.hasFile', () => { 14 | test('should tell if a given file path belongs to the project', () => { 15 | let pr = join(tmpdir(), 'gauge'); 16 | let project = new GaugeProject(pr, { langauge: 'java', plugins: [] }); 17 | assert.ok(project.hasFile(join(tmpdir(), 'gauge', 'specs', 'example.spec'))); 18 | }); 19 | 20 | test('should tell if a given file path does not belong to the project', () => { 21 | let pr = join(tmpdir(), 'gauge'); 22 | let project = new GaugeProject(pr, { langauge: 'java', plugins: [] }); 23 | assert.ok(!project.hasFile(join(tmpdir(), 'gauge2', 'specs', 'example.spec'))); 24 | }); 25 | 26 | test('should tell if a given file path belonga to the project for the root itself', () => { 27 | let pr = join(tmpdir(), 'gauge'); 28 | let project = new GaugeProject(pr, { langauge: 'java', plugins: [] }); 29 | assert.ok(project.hasFile(pr)); 30 | }); 31 | }); 32 | 33 | suite('.envs', () => { 34 | let cli: CLI; 35 | let sandbox; 36 | setup( () => { 37 | sandbox = createSandbox(); 38 | cli = sandbox.createStubInstance(CLI); 39 | }); 40 | 41 | teardown(() => { 42 | sandbox.restore(); 43 | }); 44 | test('should return envs for maven project', () => { 45 | let pr = join(tmpdir(), 'gauge'); 46 | let project = new MavenProject(pr, { langauge: 'java', plugins: [] }); 47 | let stub = sandbox.stub(child_process, 'execSync'); 48 | stub.returns("/user/local/gauge_custom_classspath/"); 49 | assert.deepEqual(project.envs(cli), { gauge_custom_classpath: '/user/local/gauge_custom_classspath/'}); 50 | }); 51 | 52 | test('should show error message for maven project', () => { 53 | let pr = join(tmpdir(), 'gauge'); 54 | let project = new MavenProject(pr, { langauge: 'java', plugins: [] }); 55 | let expectedErrorMessage; 56 | sandbox.stub(window, 'showErrorMessage').callsFake((args) => expectedErrorMessage = args ); 57 | sandbox.stub(child_process, 'execSync').throws({output: "Error message."}); 58 | project.envs(cli); 59 | assert.deepEqual(expectedErrorMessage, "Error calculating project classpath.\t\nError message."); 60 | }); 61 | 62 | test('should return envs for gradle project', () => { 63 | let pr = join(tmpdir(), 'gauge'); 64 | let project = new GradleProject(pr, { langauge: 'java', plugins: [] }); 65 | let stub = sandbox.stub(child_process, 'execSync'); 66 | stub.returns("/user/local/gauge_custom_classspath/"); 67 | assert.deepEqual(project.envs(cli), { gauge_custom_classpath: '/user/local/gauge_custom_classspath/'}); 68 | }); 69 | 70 | test('should show error message for gradle project', () => { 71 | let pr = join(tmpdir(), 'gauge'); 72 | let project = new GradleProject(pr, { langauge: 'java', plugins: [] }); 73 | let expectedErrorMessage; 74 | sandbox.stub(window, 'showErrorMessage').callsFake((args) => expectedErrorMessage = args ); 75 | sandbox.stub(child_process, 'execSync').throws({output: "Error message."}); 76 | project.envs(cli); 77 | assert.deepEqual(expectedErrorMessage, "Error calculating project classpath.\t\nError message."); 78 | }); 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /src/execution/runArgs.ts: -------------------------------------------------------------------------------- 1 | import { DebugConfiguration } from 'vscode'; 2 | import { GaugeCommands } from '../constants'; 3 | 4 | export type GaugeRunOption = { 5 | env?: string[]; 6 | failed?: boolean; 7 | 'hide-suggestion'?: boolean; 8 | n?: number; 9 | parallel?: boolean; 10 | repeat?: boolean; 11 | 'retry-only'?: string; 12 | scenario?: string[]; 13 | 'simple-console'?: boolean; 14 | tags?: string; 15 | } 16 | 17 | const commonLaunchAttributes = new Set([ 18 | 'type', 19 | 'request', 20 | 'name', 21 | 'presentation', 22 | 'preLaunchTask', 23 | 'postDebugTask', 24 | 'internalConsoleOptions', 25 | 'debugServer', 26 | 'serverReadyAction', 27 | 'windows', 28 | 'linux', 29 | 'osx' 30 | ]); 31 | 32 | const excludeCommonLaunchAtrributes = (object: object): object => 33 | Object.entries(object).filter(([key, _]) => !(commonLaunchAttributes.has(key))) 34 | .reduce((out, [k, v]) => { out[k] = v; return out; }, {}); 35 | 36 | export const extractGaugeRunOption = (configs: DebugConfiguration[]): GaugeRunOption => { 37 | if (!configs) return {}; 38 | const extracted = configs.find(c => c.type === 'gauge' && c.request === 'test') || {}; 39 | return excludeCommonLaunchAtrributes(extracted); 40 | } 41 | 42 | type BuildRunArgs = (spec: string, option: GaugeRunOption) => string[] 43 | 44 | const flag = (key: string) => `--${key}` 45 | 46 | const buildGaugeArgs: BuildRunArgs = (spec, option) => { 47 | const args: string[] = [GaugeCommands.Run]; 48 | 49 | if (option.failed) return args.concat(flag('failed')); 50 | if (option.repeat) return args.concat(flag('repeat')); 51 | 52 | args.push(...Object.entries( 53 | { 'hide-suggestion': true, 'simple-console': !option.parallel, ...option } 54 | ).map(([k, v]) => { 55 | if (typeof v === 'boolean') return v ? [flag(k)] : []; 56 | if (v instanceof Array && v.every(e => typeof e === 'string')) return [flag(k), v.join(',')]; 57 | if (typeof v === 'string' || typeof v === 'number') return [flag(k), `${v}`]; 58 | return []; 59 | }).reduce((acc, arr) => acc.concat(arr))); 60 | 61 | if (spec) args.push(spec); 62 | 63 | return args; 64 | } 65 | 66 | const buildJavaRunArgs = (spec: string, option: GaugeRunOption, prefix: String, additionalFlags: (...keys: string[]) => string) => { 67 | const { 68 | failed, repeat, tags, parallel, n, env, ...rest 69 | } = { 70 | 'hide-suggestion': true, 'simple-console': true, ...option 71 | }; 72 | const p = (str: string) => `${prefix}${str}`; 73 | const args = [] as string[]; 74 | if (failed) return args.concat(p(additionalFlags('failed'))); 75 | if (repeat) return args.concat(p(additionalFlags('repeat'))); 76 | if (parallel) { 77 | args.push(p('inParallel=true')); 78 | if (n) args.push(p(`nodes=${n}`)); 79 | } 80 | if (tags) args.push(p(`tags=${tags}`)); 81 | if (env) args.push(p(`env=${env.join(',')}`)); 82 | 83 | const flags = Object.entries(rest).filter(([, v]) => typeof v === 'boolean' && v).map(([k,]) => k); 84 | if (flags && 0 < flags.length) args.push(p(additionalFlags(...flags))); 85 | 86 | if (spec) args.push(p(`specsDir=${spec}`)); 87 | return args; 88 | } 89 | 90 | const buildGradleArgs: BuildRunArgs = (spec, option) => { 91 | const additionalFlags = (...keys: string[]) => `additionalFlags=${keys.map(flag).join(' ')}`; 92 | return ['clean', 'gauge', ...buildJavaRunArgs(spec, option, '-P', additionalFlags)]; 93 | } 94 | 95 | const buildMavenArgs: BuildRunArgs = (spec, option): string[] => { 96 | const flags = (...keys: string[]) => `flags=${keys.map(flag).join(',')}`; 97 | return ['-q', 'clean', 'compile', 'test-compile', 'gauge:execute', 98 | ...buildJavaRunArgs(spec, option, '-D', flags)]; 99 | } 100 | 101 | export const buildRunArgs = { 102 | forGauge: buildGaugeArgs, 103 | forGradle: buildGradleArgs, 104 | forMaven: buildMavenArgs, 105 | } 106 | -------------------------------------------------------------------------------- /src/config/gaugeProjectConfig.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { writeFileSync, readdirSync, existsSync } from 'fs'; 3 | import { exec } from 'child_process'; 4 | import * as xmlbuilder from 'xmlbuilder'; 5 | import GaugeConfig from './gaugeConfig'; 6 | 7 | const DEFAULT_JAVA_VERSION = '11'; 8 | 9 | export class GaugeJavaProjectConfig { 10 | projectRoot: string; 11 | gaugeConfig: GaugeConfig; 12 | constructor(projectRoot: string, private pluginVersion: string, gaugeConfig: GaugeConfig) { 13 | this.projectRoot = projectRoot; 14 | this.gaugeConfig = gaugeConfig; 15 | } 16 | 17 | private defaultCLassPath(javaVersion: string) { 18 | return [ 19 | { 20 | kind: 'con', 21 | path: `org.eclipse.jdt.launching.JRE_CONTAINER/` + 22 | `org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-${javaVersion}` 23 | }, 24 | { 25 | kind: 'src', 26 | path: 'src/test/java' 27 | }, 28 | { 29 | kind: 'output', 30 | path: 'gauge_bin' 31 | } 32 | ]; 33 | } 34 | 35 | generate() { 36 | exec('java -version', (err, __, out) => { 37 | const dotCPFilePath = path.join(this.projectRoot, '.classpath'); 38 | let javaVersion = /.*?(\d+\.?\d*\.?\d*).*/.exec(out)![1]; 39 | if (err !== null || !out) { 40 | return this.createDotClassPathFile(dotCPFilePath, DEFAULT_JAVA_VERSION); 41 | } 42 | if (javaVersion.match(/^\d\./)) { 43 | this.createDotClassPathFile(dotCPFilePath, javaVersion.replace(/(\.\d+(\w+)?$)/, '')); 44 | } else { 45 | this.createDotClassPathFile(dotCPFilePath, javaVersion.replace(/\.\d+/g, '')); 46 | } 47 | }); 48 | this.createDotProjectFile(path.join(this.projectRoot, '.project')); 49 | } 50 | 51 | private createDotClassPathFile(cpFilePath: string, javaVersion: string) { 52 | let javaPluginPath = path.join(this.gaugeConfig.pluginsPath(), 'java'); 53 | let jars = readdirSync(path.join(javaPluginPath, `${this.pluginVersion}/libs/`)) 54 | .filter((jar) => jar.match(/gauge|assertj-core/)); 55 | let classPathForJars = jars 56 | .map((jar) => this.cpEntry('lib', path.join(javaPluginPath, `${this.pluginVersion}/libs/`, jar))); 57 | let classPathObj = { 58 | classpath: { classpathentry: [...this.getDefaultCPEntries(javaVersion), ...classPathForJars] } 59 | }; 60 | this.configFile(cpFilePath, classPathObj); 61 | } 62 | 63 | private createDotProjectFile(projectFilePath) { 64 | const projectObj = { 65 | projectDescription: { 66 | name: path.basename(this.projectRoot), 67 | comment: '', 68 | projects: {}, 69 | buildSpec: { 70 | buildCommand: { 71 | name: 'org.eclipse.jdt.core.javabuilder', 72 | arguments: {} 73 | } 74 | }, 75 | natures: { 76 | nature: 'org.eclipse.jdt.core.javanature' 77 | } 78 | } 79 | }; 80 | this.configFile(projectFilePath, projectObj); 81 | } 82 | 83 | private configFile(filePath, contentObj) { 84 | if (existsSync(filePath)) return; 85 | const xml = xmlbuilder.create(contentObj, { encoding: 'UTF-8' }); 86 | let content = xml.end({ pretty: true }); 87 | writeFileSync(filePath, content); 88 | } 89 | 90 | private getDefaultCPEntries(javaVersion) { 91 | return this.defaultCLassPath(javaVersion) 92 | .map((entry) => this.cpEntry(entry.kind, entry.path)); 93 | } 94 | 95 | private cpEntry(kind, path) { 96 | return { 97 | '@kind': kind, 98 | '@path': path 99 | }; 100 | } 101 | } -------------------------------------------------------------------------------- /src/file/specificationFileProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as fs from "fs-extra"; 4 | import * as path from "path"; 5 | import { EOL } from "os"; 6 | import { CancellationToken, Disposable, Position, Range, TextDocument, commands, window, workspace } from "vscode"; 7 | import { CancellationTokenSource } from "vscode-languageclient"; 8 | import { GaugeRequests, GaugeVSCodeCommands } from "../constants"; 9 | import { GaugeWorkspace } from "../gaugeWorkspace"; 10 | 11 | export class SpecificationProvider extends Disposable { 12 | 13 | private readonly gaugeWorkspace: GaugeWorkspace; 14 | private _disposable: Disposable; 15 | 16 | constructor(workspace: GaugeWorkspace) { 17 | super(() => this.dispose()); 18 | this.gaugeWorkspace = workspace; 19 | this._disposable = Disposable.from( 20 | commands.registerCommand(GaugeVSCodeCommands.CreateSpecification, () => { 21 | if (this.gaugeWorkspace.getClientsMap().size > 1) { 22 | return this.gaugeWorkspace.showProjectOptions((selection: string) => { 23 | return this.createSpecificationIn(selection); 24 | }); 25 | } 26 | return this.createSpecificationIn(this.gaugeWorkspace.getDefaultFolder()); 27 | }) 28 | ); 29 | } 30 | 31 | private createSpecificationIn(project: string): Thenable { 32 | let client = this.gaugeWorkspace.getClientsMap().get(project).client; 33 | let token = new CancellationTokenSource().token; 34 | return client.sendRequest(GaugeRequests.SpecDirs, token).then((specDirs: any) => { 35 | let p = "Choose the folder in which the specification should be created"; 36 | if (specDirs.length > 1) { 37 | return window.showQuickPick(specDirs, { canPickMany: false, placeHolder: p }).then((dir: string) => { 38 | if (!dir) return; 39 | return this.getFileName(token, path.join(project, dir)); 40 | }, this.handleError); 41 | } 42 | return this.getFileName(token, path.join(project, specDirs[0])); 43 | 44 | }, this.handleError); 45 | } 46 | 47 | private getFileName(token: CancellationToken, dir: string): Thenable { 48 | return window.showInputBox({ placeHolder: "Enter the file name" }, token).then((file) => { 49 | if (!file) return; 50 | let filename = path.join(dir, file + ".spec"); 51 | if (fs.existsSync(filename)) return this.handleError("File" + filename + " already exists."); 52 | return this.createFileAndShow(filename); 53 | }, this.handleError); 54 | } 55 | 56 | private createFileAndShow(filename: string): Thenable { 57 | let info = this.getDocumentInfo(); 58 | return fs.createFile(filename).then(() => { 59 | return fs.writeFile(filename, info.text, "utf-8").then(() => { 60 | return workspace.openTextDocument(filename).then((doc: TextDocument) => { 61 | return window.showTextDocument(doc, { selection: info.range }); 62 | }, this.handleError); 63 | }, this.handleError); 64 | }, this.handleError); 65 | } 66 | 67 | private getDocumentInfo(): { text: string, range: Range } { 68 | let withHelp = workspace.getConfiguration("gauge").get("create.specification.withHelp"); 69 | let text = "# SPECIFICATION HEADING" + EOL; 70 | if (withHelp) { 71 | text += EOL + "This is an executable specification file. This file follows markdown syntax." + EOL + 72 | "Every heading in this file denotes a scenario. Every bulleted point denotes a step." + EOL + EOL + 73 | "> To turn off these comments, set the configuration" + 74 | "`gauge.create.specification.withHelp` to false." + EOL; 75 | } 76 | text += EOL + "## SCENARIO HEADING" + EOL + EOL + "* step" + EOL; 77 | let line = text.split(EOL).length - 2; 78 | let range = new Range(new Position(line, 2), new Position(line, 6)); 79 | return { text: text, range: range }; 80 | } 81 | 82 | handleError(message: string): Thenable { 83 | return window.showErrorMessage('Unable to generate specification. ' + message); 84 | } 85 | 86 | dispose() { 87 | this._disposable.dispose(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/annotator/generateStub.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as path from 'path'; 4 | import { CancellationTokenSource, env, commands, Disposable, window } from "vscode"; 5 | import { LanguageClient } from "vscode-languageclient/node"; 6 | import { COPY_TO_CLIPBOARD, GaugeRequests, GaugeVSCodeCommands, NEW_FILE } from "../constants"; 7 | import { GaugeClients } from "../gaugeClients"; 8 | import { ProjectFactory } from "../project/projectFactory"; 9 | import { WorkspaceEditor } from "../refactor/workspaceEditor"; 10 | import { FileListItem } from "../types/fileListItem"; 11 | 12 | export class GenerateStubCommandProvider implements Disposable { 13 | private readonly _clientsMap: GaugeClients; 14 | private readonly _disposable: Disposable; 15 | 16 | constructor(clients: GaugeClients) { 17 | this._clientsMap = clients; 18 | this._disposable = Disposable.from( 19 | commands.registerCommand(GaugeVSCodeCommands.GenerateStepStub, (code: string) => { 20 | return this.generateStepStub(code); 21 | }), commands.registerCommand(GaugeVSCodeCommands.GenerateConceptStub, (conceptInfo: any) => { 22 | return this.generateConceptStub(conceptInfo); 23 | }) 24 | ); 25 | } 26 | private generateConceptStub(conceptInfo: any) { 27 | let project = ProjectFactory.getProjectByFilepath(window.activeTextEditor.document.uri.fsPath); 28 | let languageClient = this._clientsMap.get(project.root()).client; 29 | let t = new CancellationTokenSource().token; 30 | languageClient.sendRequest(GaugeRequests.Files, { concept: true }, t).then((files: string[]) => { 31 | window.showQuickPick(this.getFileLists(files, project.root(), false)).then((selected) => { 32 | if (!selected) return; 33 | conceptInfo.conceptFile = selected.value; 34 | conceptInfo.dir = path.dirname(window.activeTextEditor.document.uri.fsPath); 35 | let token = new CancellationTokenSource().token; 36 | this.generateInFile(GaugeRequests.GenerateConcept, conceptInfo, languageClient); 37 | }, this.handleError); 38 | }, this.handleError); 39 | } 40 | 41 | private generateStepStub(code: string) { 42 | let pc = this._clientsMap.get(window.activeTextEditor.document.uri.fsPath); 43 | let token = new CancellationTokenSource().token; 44 | pc.client.sendRequest(GaugeRequests.Files, token).then((files: string[]) => { 45 | window.showQuickPick(this.getFileLists(files, pc.project.root())).then((selected: FileListItem) => { 46 | if (!selected) return; 47 | if (selected.isCopyToClipBoard()) { 48 | env.clipboard.writeText(code).then(() => { 49 | window.showInformationMessage("Step Implementation copied to clipboard"); 50 | }, this.handleError); 51 | } else { 52 | let params = { implementationFilePath: selected.value, codes: [code] }; 53 | this.generateInFile(GaugeRequests.AddStub, params, pc.client); 54 | } 55 | }, this.handleError); 56 | }, this.handleError); 57 | } 58 | 59 | private generateInFile(request: string, params: any, languageClient: LanguageClient) { 60 | let token = new CancellationTokenSource().token; 61 | languageClient.sendRequest(request, params, token).then((e) => { 62 | languageClient.protocol2CodeConverter.asWorkspaceEdit(e).then((edit) =>{ 63 | new WorkspaceEditor(edit).applyChanges() 64 | }, this.handleError); 65 | }, this.handleError); 66 | } 67 | 68 | private handleError(reason: string) { 69 | window.showErrorMessage('Unable to generate implementation. ' + reason); 70 | } 71 | 72 | private getFileLists(files: string[], cwd: string, copy = true): FileListItem[] { 73 | const showFileList: FileListItem[] = files.map((file) => { 74 | return new FileListItem(path.basename(file), path.relative(cwd, path.dirname(file)), file); 75 | }); 76 | const quickPickFileList = [new FileListItem(NEW_FILE, "Create a new file", NEW_FILE)]; 77 | if (copy) { 78 | quickPickFileList.push(new FileListItem(COPY_TO_CLIPBOARD, "", COPY_TO_CLIPBOARD)); 79 | } 80 | return quickPickFileList.concat(showFileList); 81 | } 82 | 83 | dispose() { 84 | this._disposable.dispose(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/config/configProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Disposable, ExtensionContext, window, commands, workspace, ConfigurationTarget } from "vscode"; 4 | import { VSCodeCommands, GaugeVSCodeCommands } from "../constants"; 5 | 6 | const FILE_ASSOCIATIONS_KEY = "files.associations"; 7 | 8 | export class ConfigProvider extends Disposable { 9 | private recommendedSettings = { 10 | "files.autoSave": "afterDelay", 11 | "files.autoSaveDelay": 500 12 | }; 13 | 14 | private _disposable: Disposable; 15 | 16 | constructor(private context: ExtensionContext) { 17 | super(() => this.dispose()); 18 | 19 | this.applyDefaultSettings(); 20 | this._disposable = commands.registerCommand(GaugeVSCodeCommands.SaveRecommendedSettings, 21 | () => this.applyAndReload(this.recommendedSettings, ConfigurationTarget.Workspace)); 22 | 23 | if (!this.verifyRecommendedConfig()) { 24 | let config = workspace.getConfiguration().inspect("gauge.recommendedSettings.options"); 25 | if (config.globalValue === "Apply & Reload") { 26 | let settings = {...this.recommendedSettings}; 27 | this.applyAndReload(settings, ConfigurationTarget.Workspace); 28 | return; 29 | } 30 | window.showInformationMessage("Gauge recommends " + 31 | "some settings for best experience with Visual Studio Code.", 32 | "Apply & Reload", "Remind me later", "Ignore") 33 | .then((option) => { 34 | if (option === "Apply & Reload") { 35 | this.applyAndReload(this.recommendedSettings, ConfigurationTarget.Workspace, false); 36 | let settings = {"gauge.recommendedSettings.options": "Apply & Reload"}; 37 | return this.applyAndReload(settings, ConfigurationTarget.Global); 38 | } else if (option === "Ignore") { 39 | let settings = { "gauge.recommendedSettings.options": "Ignore" }; 40 | return this.applyAndReload(settings, ConfigurationTarget.Global, false); 41 | } else if (option === "Remind me later") { 42 | let config = workspace.getConfiguration().inspect("gauge.recommendedSettings.options"); 43 | if (config.globalValue !== "Remind me later") { 44 | let settings = { "gauge.recommendedSettings.options": "Remind me later" }; 45 | return this.applyAndReload(settings, ConfigurationTarget.Global, false); 46 | } 47 | } 48 | }); 49 | } 50 | } 51 | 52 | private applyDefaultSettings() { 53 | let workspaceConfig = workspace.getConfiguration().inspect(FILE_ASSOCIATIONS_KEY).workspaceValue; 54 | let recomendedConfig = {}; 55 | if (!!workspaceConfig) recomendedConfig = workspaceConfig; 56 | recomendedConfig["*.spec"] = "gauge"; 57 | recomendedConfig["*.cpt"] = "gauge"; 58 | workspace.getConfiguration().update(FILE_ASSOCIATIONS_KEY, recomendedConfig, ConfigurationTarget.Workspace); 59 | } 60 | 61 | private verifyRecommendedConfig(): boolean { 62 | let config = workspace.getConfiguration().inspect("gauge.recommendedSettings.options"); 63 | if (config.globalValue === "Ignore") return true; 64 | for (const key in this.recommendedSettings) { 65 | if (this.recommendedSettings.hasOwnProperty(key)) { 66 | let configVal = workspace.getConfiguration().inspect(key); 67 | if (!configVal.workspaceFolderValue && !configVal.workspaceValue && 68 | configVal.globalValue !== this.recommendedSettings[key]) { 69 | return false; 70 | } 71 | } 72 | } 73 | return true; 74 | } 75 | 76 | private applyAndReload(settings: Object, configurationTarget: number, shouldReload: boolean = true): Thenable { 77 | let updatePromises = []; 78 | for (const key in settings) { 79 | if (settings.hasOwnProperty(key)) { 80 | updatePromises.push(workspace.getConfiguration() 81 | .update(key, settings[key], configurationTarget)); 82 | } 83 | } 84 | if (!shouldReload) return; 85 | return Promise.all(updatePromises).then(() => commands.executeCommand(VSCodeCommands.ReloadWindow)); 86 | } 87 | 88 | dispose() { 89 | this._disposable.dispose(); 90 | } 91 | } -------------------------------------------------------------------------------- /test/execution/execution.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as path from 'path'; 3 | import { commands, Selection, Uri, window, workspace } from 'vscode'; 4 | import { GaugeVSCodeCommands } from '../../src/constants'; 5 | import { createSandbox } from 'sinon'; 6 | 7 | let testDataPath = path.join(__dirname, '..', '..', '..', 'test', 'testdata', 'sampleProject'); 8 | 9 | suite('Gauge Execution Tests', () => { 10 | let sandbox; 11 | 12 | teardown(() => { 13 | sandbox.restore(); 14 | }); 15 | 16 | setup(async () => { 17 | sandbox = createSandbox(); 18 | await commands.executeCommand('workbench.action.closeAllEditors'); 19 | }); 20 | 21 | let assertStatus = (status, val = true) => { 22 | let logDoc = workspace.textDocuments.find((x) => x.languageId === "Log"); 23 | let output = logDoc && logDoc.getText() || "Couldn't find the log output."; 24 | assert.equal(status, val, "Output:\n\n" + output); 25 | }; 26 | 27 | teardown(async () => { 28 | await commands.executeCommand(GaugeVSCodeCommands.StopExecution); 29 | await commands.executeCommand('workbench.action.closeAllEditors'); 30 | }); 31 | 32 | test('should execute given specification', async () => { 33 | let spec = path.join(testDataPath, 'specs', 'example.spec'); 34 | let doc = await workspace.openTextDocument(Uri.file(spec)); 35 | await window.showTextDocument(doc); 36 | let status = await commands.executeCommand(GaugeVSCodeCommands.Execute, spec); 37 | assertStatus(status); 38 | }).timeout(30000); 39 | 40 | test('should execute given scenario', async () => { 41 | let spec = Uri.file(path.join(testDataPath, 'specs', 'example.spec')); 42 | let doc = await workspace.openTextDocument(spec); 43 | await window.showTextDocument(doc); 44 | let scenario = spec.fsPath + ":6"; 45 | let status = await commands.executeCommand(GaugeVSCodeCommands.Execute, scenario); 46 | assertStatus(status); 47 | }).timeout(30000); 48 | 49 | test('should execute all specification in spec dir', async () => { 50 | let status = await commands.executeCommand(GaugeVSCodeCommands.ExecuteAllSpecs); 51 | assertStatus(status); 52 | }).timeout(30000); 53 | 54 | test('should execute currently open specification', async () => { 55 | let specFile = Uri.file(path.join(testDataPath, 'specs', 'example.spec')); 56 | let doc = await workspace.openTextDocument(specFile); 57 | await window.showTextDocument(doc); 58 | let status = await commands.executeCommand(GaugeVSCodeCommands.ExecuteSpec); 59 | assertStatus(status); 60 | }).timeout(30000); 61 | 62 | test('should execute scenario at cursor', async () => { 63 | let specFile = Uri.file(path.join(testDataPath, 'specs', 'example.spec')); 64 | let doc = await workspace.openTextDocument(specFile); 65 | await window.showTextDocument(doc); 66 | await commands.executeCommand("workbench.action.focusFirstEditorGroup"); 67 | window.activeTextEditor.selection = new Selection(8, 0, 8, 0); 68 | let status = await commands.executeCommand(GaugeVSCodeCommands.ExecuteScenario); 69 | assertStatus(status); 70 | }).timeout(30000); 71 | 72 | test('should abort execution', async () => { 73 | let spec = path.join(testDataPath, 'specs', 'example.spec'); 74 | let doc = await workspace.openTextDocument(Uri.file(spec)); 75 | await window.showTextDocument(doc); 76 | // simulate a delay, we could handle this in executor, i.e. before spawining an execution 77 | // check if an abort signal has been sent. 78 | // It seems like over-complicating things for a non-human scenario :) 79 | setTimeout(() => commands.executeCommand(GaugeVSCodeCommands.StopExecution), 100); 80 | let status = await commands.executeCommand(GaugeVSCodeCommands.Execute, spec); 81 | assertStatus(status, false); 82 | }).timeout(30000); 83 | 84 | test('should reject execution when another is already in progress', async () => { 85 | let expectedErrorMessage; 86 | sandbox.stub(window, 'showErrorMessage').callsFake((args) => expectedErrorMessage = args ); 87 | 88 | let spec = path.join(testDataPath, 'specs', 'example.spec'); 89 | let doc = await workspace.openTextDocument(Uri.file(spec)); 90 | await window.showTextDocument(doc); 91 | commands.executeCommand(GaugeVSCodeCommands.ExecuteAllSpecs); 92 | try { 93 | await commands.executeCommand(GaugeVSCodeCommands.Execute, spec); 94 | throw new Error("Must not run new tests when others are already in progress"); 95 | } catch { 96 | assert.equal(expectedErrorMessage, "A Specification or Scenario is still running!"); 97 | } 98 | }).timeout(30000); 99 | }); 100 | -------------------------------------------------------------------------------- /src/init/projectInit.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as fs from 'fs-extra'; 4 | import * as path from 'path'; 5 | import { commands, Disposable, Progress, Uri, window, workspace } from 'vscode'; 6 | import { CLI } from '../cli'; 7 | import { GaugeCommands, GaugeVSCodeCommands, INSTALL_INSTRUCTION_URI, VSCodeCommands } from "../constants"; 8 | import { FileListItem } from '../types/fileListItem'; 9 | 10 | export class ProjectInitializer extends Disposable { 11 | private readonly _disposable: Disposable; 12 | 13 | private readonly cli: CLI; 14 | 15 | constructor(cli: CLI) { 16 | super(() => this.dispose()); 17 | this.cli = cli; 18 | this._disposable = commands.registerCommand(GaugeVSCodeCommands.CreateProject, async () => { 19 | await this.createProject(); 20 | }); 21 | } 22 | 23 | dispose() { 24 | this._disposable.dispose(); 25 | } 26 | 27 | private async createProject() { 28 | if (!this.cli.isGaugeInstalled()) { 29 | window.showErrorMessage("Please install gauge to create a new Gauge project." + 30 | `For more info please refer the [install intructions](${INSTALL_INSTRUCTION_URI}).`); 31 | return; 32 | } 33 | const tmpl = await window.showQuickPick(await this.getTemplatesList()); 34 | if (!tmpl) return; 35 | let folders = await this.getTargetFolder(); 36 | if (!folders) return; 37 | let options: any = { prompt: "Enter a name for your new project", placeHolder: "gauge-tests" }; 38 | const name = await window.showInputBox(options); 39 | if (!name) return; 40 | const projectFolderUri = Uri.file(path.join(folders[0].fsPath, name)); 41 | if (fs.existsSync(projectFolderUri.fsPath)) { 42 | return this.handleError( 43 | null, `A folder named ${name} already exists in ${folders[0].fsPath}`, projectFolderUri.fsPath, false); 44 | } 45 | fs.mkdirSync(projectFolderUri.fsPath); 46 | return this.createProjectInDir(tmpl, projectFolderUri); 47 | } 48 | 49 | private async getTargetFolder() { 50 | let options = { 51 | canSelectFolders: true, 52 | openLabel: "Select a folder to create the project in", 53 | canSelectMany: false 54 | }; 55 | const folders = await window.showOpenDialog(options); 56 | return folders; 57 | } 58 | 59 | private async createProjectInDir(template: FileListItem, projectFolder: Uri) { 60 | return window.withProgress({ location: 10 }, async (p: Progress<{}>) => { 61 | return new Promise(async (res, rej) => { 62 | let ph = new ProgressHandler(p, res, rej); 63 | await this.createFromCommandLine(template, projectFolder, ph); 64 | }); 65 | }); 66 | } 67 | 68 | private async createFromCommandLine(template: FileListItem, projectFolder: Uri, p: ProgressHandler) { 69 | let args = [GaugeCommands.Init, template.label]; 70 | const cmd = this.cli.gaugeCommand(); 71 | p.report("Initializing project..."); 72 | let proc = cmd.spawn(args, { cwd: projectFolder.fsPath, env: process.env }); 73 | proc.addListener('error', async (err) => { 74 | this.handleError(p, "Failed to create template. " + err.message, projectFolder.fsPath); 75 | }); 76 | proc.stdout.on('data', (m) => { console.log(m.toString()); }); 77 | proc.on('close', async (code) => { 78 | if (code === 0) { p.cancel("Faile to initialize project."); } 79 | await p.end(projectFolder); 80 | }); 81 | } 82 | 83 | private async getTemplatesList(): Promise> { 84 | let args = ["template", "--list", "--machine-readable"]; 85 | let cp = this.cli.gaugeCommand().spawnSync(args, { env: process.env }); 86 | try { 87 | let _templates = JSON.parse(cp.stdout.toString()); 88 | return _templates.map((tmpl) => new FileListItem(tmpl.key, tmpl.Description, tmpl.value)); 89 | } catch (error) { 90 | await window.showErrorMessage("Failed to get list of templates.", 91 | " Try running 'gauge template --list ----machine-readable' from command line"); 92 | return []; 93 | } 94 | } 95 | 96 | private handleError(p: ProgressHandler, err, dirPath: string, removeDir: boolean = true) { 97 | if (removeDir) fs.removeSync(dirPath); 98 | if (p) p.cancel(err); 99 | return window.showErrorMessage(err); 100 | } 101 | } 102 | 103 | class ProgressHandler { 104 | private progress: Progress<{ message: string }>; 105 | resolve: (value?: {} | PromiseLike<{}>) => void; 106 | reject: (reason?: any) => void; 107 | 108 | constructor(progress, resolve, reject) { 109 | this.progress = progress; 110 | this.resolve = resolve; 111 | this.reject = reject; 112 | } 113 | 114 | report(message) { 115 | this.progress.report({ message: message }); 116 | } 117 | 118 | async end(uri: Uri) { 119 | this.resolve(); 120 | commands.executeCommand(VSCodeCommands.OpenFolder, uri, !!workspace.workspaceFolders); 121 | } 122 | 123 | cancel(message: string | Buffer) { 124 | this.reject(message.toString()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { debug, ExtensionContext, languages, window, workspace, ConfigurationTarget } from 'vscode'; 4 | import { GenerateStubCommandProvider } from './annotator/generateStub'; 5 | import { CLI } from './cli'; 6 | import { ConfigProvider } from './config/configProvider'; 7 | import { ReferenceProvider } from './gaugeReference'; 8 | import { GaugeState } from './gaugeState'; 9 | import { GaugeWorkspace } from './gaugeWorkspace'; 10 | import { ProjectInitializer } from './init/projectInit'; 11 | import { ProjectFactory } from './project/projectFactory'; 12 | import { hasActiveGaugeDocument } from './util'; 13 | import { showInstallGaugeNotification, showWelcomeNotification } from './welcomeNotifications'; 14 | import { GaugeClients as GaugeProjectClientMap } from './gaugeClients'; 15 | import { GaugeSemanticTokensProvider, legend } from './semanticTokensProvider'; 16 | 17 | const MINIMUM_SUPPORTED_GAUGE_VERSION = '0.9.6'; 18 | 19 | const clientsMap: GaugeProjectClientMap = new GaugeProjectClientMap(); 20 | 21 | // This function reads Gauge-specific semantic token colors from the configuration 22 | // and then updates the editor.semanticTokenColorCustomizations setting. 23 | function updateGaugeSemanticTokenColors() { 24 | // Read Gauge settings from the gauge configuration section. 25 | const gaugeConfig = workspace.getConfiguration("gauge.semanticTokenColors"); 26 | const colors = { 27 | argument: gaugeConfig.get("argument"), 28 | stepMarker: gaugeConfig.get("stepMarker"), 29 | step: gaugeConfig.get("step"), 30 | table: gaugeConfig.get("table"), 31 | tableHeaderSeparator: gaugeConfig.get("tableHeaderSeparator"), 32 | tableBorder: gaugeConfig.get("tableBorder"), 33 | tagKeyword: gaugeConfig.get("tagKeyword"), 34 | tagValue: gaugeConfig.get("tagValue"), 35 | specification: gaugeConfig.get("specification"), 36 | scenario: gaugeConfig.get("scenario"), 37 | comment: gaugeConfig.get("comment"), 38 | disabledStep: gaugeConfig.get("disabledStep") 39 | }; 40 | 41 | // Build a new set of semantic token color rules. 42 | const semanticTokenRules = { 43 | "argument": { "foreground": colors.argument }, 44 | "stepMarker": { "foreground": colors.stepMarker }, 45 | "step": { "foreground": colors.step }, 46 | "table": { "foreground": colors.table }, 47 | "tableHeaderSeparator": { "foreground": colors.tableHeaderSeparator }, 48 | "tableBorder": { "foreground": colors.tableBorder }, 49 | "tagKeyword": { "foreground": colors.tagKeyword }, 50 | "tagValue": { "foreground": colors.tagValue }, 51 | "specification": { "foreground": colors.specification }, 52 | "scenario": { "foreground": colors.scenario }, 53 | "gaugeComment": { "foreground": colors.comment }, 54 | "disabledStep": { "foreground": colors.disabledStep } 55 | }; 56 | 57 | // Get the current global editor configuration. 58 | const editorConfig = workspace.getConfiguration("editor"); 59 | 60 | // Update the semantic token color customizations. 61 | editorConfig.update("semanticTokenColorCustomizations", { rules: semanticTokenRules }, ConfigurationTarget.Global); 62 | } 63 | 64 | export async function activate(context: ExtensionContext) { 65 | let cli = CLI.instance(); 66 | if (!cli) { 67 | return; 68 | } 69 | let folders = workspace.workspaceFolders; 70 | context.subscriptions.push(new ProjectInitializer(cli)); 71 | let hasGaugeProject = folders && folders.some((f) => ProjectFactory.isGaugeProject(f.uri.fsPath)); 72 | if (!hasActiveGaugeDocument(window.activeTextEditor) && !hasGaugeProject) return; 73 | if (!cli.isGaugeInstalled() || !cli.isGaugeVersionGreaterOrEqual(MINIMUM_SUPPORTED_GAUGE_VERSION)) { 74 | return showInstallGaugeNotification(); 75 | } 76 | showWelcomeNotification(context); 77 | languages.setLanguageConfiguration('gauge', { wordPattern: /^(?:[*])([^*].*)$/g }); 78 | let gaugeWorkspace = new GaugeWorkspace(new GaugeState(context), cli, clientsMap); 79 | updateGaugeSemanticTokenColors(); 80 | 81 | context.subscriptions.push( 82 | gaugeWorkspace, 83 | new ReferenceProvider(clientsMap), 84 | new GenerateStubCommandProvider(clientsMap), 85 | new ConfigProvider(context), 86 | debug.registerDebugConfigurationProvider('gauge', 87 | { 88 | resolveDebugConfiguration: () => { 89 | throw Error("Starting with the Gauge debug configuration is not supported. Please use the 'Gauge' commands instead."); 90 | } 91 | }), 92 | languages.registerDocumentSemanticTokensProvider( 93 | { language: 'gauge' }, 94 | new GaugeSemanticTokensProvider(), 95 | legend 96 | ), 97 | workspace.onDidChangeConfiguration((e) => { 98 | if (e.affectsConfiguration("gauge.semanticTokenColors")) { 99 | updateGaugeSemanticTokenColors(); 100 | } 101 | }) 102 | ); 103 | } 104 | 105 | export function deactivate(): Thenable { 106 | const promises: Thenable[] = []; 107 | 108 | for (const { client } of clientsMap.values()) { 109 | promises.push(client.stop()); 110 | } 111 | return Promise.all(promises).then(() => undefined); 112 | } -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { ChildProcess, CommonSpawnOptions, spawn, spawnSync, SpawnSyncReturns } from 'child_process'; 4 | import { platform } from 'os'; 5 | import { window } from 'vscode'; 6 | import { GaugeCommands, GRADLE_COMMAND, MAVEN_COMMAND } from './constants'; 7 | import { OutputChannel } from './execution/outputChannel'; 8 | 9 | export class CLI { 10 | private readonly _gaugeVersion: string; 11 | private readonly _gaugeCommitHash: string; 12 | private readonly _gaugePlugins: Array; 13 | private readonly _gaugeCommand: Command; 14 | private readonly _mvnCommand: Command; 15 | private readonly _gradleCommand: Command; 16 | 17 | public constructor(cmd: Command, manifest: any, mvnCommand: Command, gradleCommand: Command) { 18 | this._gaugeCommand = cmd; 19 | this._mvnCommand = mvnCommand; 20 | this._gradleCommand = gradleCommand; 21 | this._gaugeVersion = manifest.version; 22 | this._gaugeCommitHash = manifest.commitHash; 23 | this._gaugePlugins = manifest.plugins; 24 | } 25 | 26 | public static instance(): CLI { 27 | const gaugeCommand = this.getCommand(GaugeCommands.Gauge); 28 | const mvnCommand = this.getCommand(MAVEN_COMMAND); 29 | const gradleCommand = this.getGradleCommand(); 30 | if (!gaugeCommand) return new CLI(undefined, {}, mvnCommand, gradleCommand); 31 | let gv = gaugeCommand.spawnSync([GaugeCommands.Version, GaugeCommands.MachineReadable]); 32 | let gaugeVersionInfo; 33 | try { 34 | gaugeVersionInfo = JSON.parse(gv.stdout.toString()); 35 | } catch (e) { 36 | window.showErrorMessage(`Error fetching Gauge and plugins version information. \n${gv.stdout.toString()}`); 37 | return; 38 | } 39 | return new CLI(gaugeCommand, gaugeVersionInfo, mvnCommand, gradleCommand); 40 | } 41 | 42 | public isPluginInstalled(pluginName: string): boolean { 43 | return this._gaugePlugins.some((p: any) => p.name === pluginName); 44 | } 45 | 46 | public gaugeCommand(): Command { 47 | return this._gaugeCommand; 48 | } 49 | 50 | public isGaugeInstalled(): boolean { 51 | return !!this._gaugeCommand; 52 | } 53 | 54 | public isGaugeVersionGreaterOrEqual(version: string): boolean { 55 | return this._gaugeVersion >= version; 56 | } 57 | 58 | public getGaugePluginVersion(language: string): any { 59 | return this._gaugePlugins.find((p) => p.name === language).version; 60 | } 61 | 62 | public async installGaugeRunner(language: string): Promise { 63 | let oc = window.createOutputChannel("Gauge Install"); 64 | let chan = new OutputChannel(oc, `Installing gauge ${language} plugin ...\n`, ""); 65 | return new Promise((resolve, reject) => { 66 | let childProcess = this._gaugeCommand.spawn([GaugeCommands.Install, language]); 67 | childProcess.stdout.on('data', (chunk) => chan.appendOutBuf(chunk.toString())); 68 | childProcess.stderr.on('data', (chunk) => chan.appendErrBuf(chunk.toString())); 69 | childProcess.on('exit', (code) => { 70 | let postFailureMessage = '\nRefer to https://docs.gauge.org/plugin.html to install manually'; 71 | chan.onFinish(resolve, code, "", postFailureMessage, false); 72 | }); 73 | }); 74 | } 75 | 76 | public mavenCommand(): Command { 77 | return this._mvnCommand; 78 | } 79 | 80 | public gradleCommand() { 81 | return this._gradleCommand; 82 | } 83 | 84 | public gaugeVersionString(): string { 85 | let v = `Gauge version: ${this._gaugeVersion}`; 86 | let cm = this._gaugeCommitHash && `Commit Hash: ${this._gaugeCommitHash}` || ''; 87 | let plugins = this._gaugePlugins 88 | .map((p: any) => p.name + ' (' + p.version + ')') 89 | .join('\n'); 90 | plugins = `Plugins\n-------\n${plugins}`; 91 | return `${v}\n${cm}\n\n${plugins}`; 92 | } 93 | 94 | public static getCommandCandidates(command: string): Command[] { 95 | return platform() === 'win32' ? [ 96 | new Command(command, ".exe"), 97 | new Command(command, ".bat", true), 98 | new Command(command, ".cmd", true), 99 | ] : [ 100 | new Command(command) 101 | ] 102 | } 103 | 104 | public static isSpawnable(command: Command): boolean { 105 | const result = command.spawnSync(); 106 | return result.status === 0 && !result.error; 107 | } 108 | 109 | private static getCommand(command: string): Command | undefined { 110 | for (const candidate of this.getCommandCandidates(command)) { 111 | if (this.isSpawnable(candidate)) return candidate; 112 | } 113 | } 114 | 115 | private static getGradleCommand() { 116 | return platform() === 'win32' ? new Command(GRADLE_COMMAND, ".bat", true) : new Command(`./${GRADLE_COMMAND}`); 117 | } 118 | } 119 | 120 | export type PlatformDependentSpawnOptions = { 121 | shell?: boolean 122 | } 123 | 124 | export class Command { 125 | public readonly command: string 126 | public readonly defaultSpawnOptions: PlatformDependentSpawnOptions 127 | 128 | constructor(public readonly cmdPrefix: string, public readonly cmdSuffix: string = "", public readonly shellMode: boolean = false) { 129 | this.command = this.cmdPrefix + this.cmdSuffix; 130 | this.defaultSpawnOptions = this.shellMode ? { shell: true } : {}; 131 | } 132 | 133 | spawn(args: string[] = [], options: CommonSpawnOptions = {}): ChildProcess { 134 | return spawn(this.command, this.argsForSpawnType(args), { ...options, ...this.defaultSpawnOptions }); 135 | } 136 | 137 | spawnSync(args: string[] = [], options: CommonSpawnOptions = {}): SpawnSyncReturns { 138 | return spawnSync(this.command, this.argsForSpawnType(args), { ...options, ...this.defaultSpawnOptions }); 139 | } 140 | 141 | // See https://github.com/nodejs/node/issues/38490 142 | argsForSpawnType(args: string[]): string[] { 143 | return this.shellMode ? args.map(arg => arg.indexOf(" ") !== -1 ? `"${arg}"` : arg) : args; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/execution/debug.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import getPort = require('get-port'); 4 | import { debug, DebugSession, Uri, workspace } from 'vscode'; 5 | import { GaugeRunners } from '../constants'; 6 | import { GaugeClients } from '../gaugeClients'; 7 | import { ExecutionConfig } from './executionConfig'; 8 | 9 | const GAUGE_DEBUGGER_NAME = "Gauge Debugger"; 10 | const REQUEST_TYPE = "attach"; 11 | 12 | export class GaugeDebugger { 13 | 14 | private debugPort: number; 15 | private languageId: string; 16 | private projectRoot: string; 17 | private debug: boolean; 18 | private dotnetProcessID: number; 19 | private config: ExecutionConfig; 20 | private clientsMap: GaugeClients; 21 | 22 | constructor(clientLanguageMap: Map, clientsMap: GaugeClients, config: ExecutionConfig) { 23 | this.languageId = clientLanguageMap.get(config.getProject().root()); 24 | this.clientsMap = clientsMap; 25 | this.config = config; 26 | this.projectRoot = this.config.getProject().root(); 27 | this.debug = config.getDebug(); 28 | } 29 | 30 | private getDebuggerConf(): any { 31 | switch (this.languageId) { 32 | case "python": { 33 | return { 34 | type: "python", 35 | name: GAUGE_DEBUGGER_NAME, 36 | request: REQUEST_TYPE, 37 | port: this.debugPort, 38 | localRoot: this.projectRoot 39 | }; 40 | } 41 | 42 | case "javascript": { 43 | return { 44 | type: "node", 45 | name: GAUGE_DEBUGGER_NAME, 46 | request: REQUEST_TYPE, 47 | port: this.debugPort, 48 | protocol: "inspector" 49 | }; 50 | } 51 | case "typescript": { 52 | return { 53 | type: "node", 54 | name: GAUGE_DEBUGGER_NAME, 55 | runtimeArgs: ["--nolazy", "-r", "ts-node/register"], 56 | request: REQUEST_TYPE, 57 | sourceMaps: true, 58 | port: this.debugPort, 59 | protocol: "inspector" 60 | }; 61 | } 62 | 63 | case "ruby": { 64 | return { 65 | name: GAUGE_DEBUGGER_NAME, 66 | type: "Ruby", 67 | request: REQUEST_TYPE, 68 | cwd: this.projectRoot, 69 | remoteWorkspaceRoot: this.projectRoot, 70 | remoteHost: "127.0.0.1", 71 | remotePort: this.debugPort 72 | }; 73 | } 74 | case "csharp": { 75 | let configobject: ConfigObj; 76 | configobject = { 77 | name: GAUGE_DEBUGGER_NAME, 78 | type: "coreclr", 79 | request: REQUEST_TYPE, 80 | processId: this.dotnetProcessID, 81 | justMyCode: true, 82 | sourceFileMap: {} 83 | }; 84 | this.updateConfigFromVscodeDebugConfig(configobject); 85 | return configobject; 86 | 87 | } 88 | case "java": { 89 | return { 90 | name: GAUGE_DEBUGGER_NAME, 91 | type: "java", 92 | request: REQUEST_TYPE, 93 | hostName: "127.0.0.1", 94 | port: this.debugPort 95 | }; 96 | } 97 | } 98 | } 99 | 100 | private updateConfigFromVscodeDebugConfig(configobject: ConfigObj) { 101 | try { 102 | let workspacePath = workspace.workspaceFolders.find((f) => f.uri.fsPath === this.projectRoot).uri; 103 | const config = workspace.getConfiguration('launch', workspacePath); 104 | const values = config.get('configurations'); 105 | configobject.sourceFileMap = values[0].sourceFileMap; 106 | configobject.justMyCode = values[0].justMyCode; 107 | } catch (ex) { 108 | console.log(ex); 109 | } 110 | } 111 | 112 | addProcessId(pid: number): any { 113 | this.dotnetProcessID = pid; 114 | } 115 | 116 | public addDebugEnv(): Thenable { 117 | let env = Object.create(process.env); 118 | if (this.debug) { 119 | env.DEBUGGING = true; 120 | env.use_nested_specs = "false"; 121 | env.SHOULD_BUILD_PROJECT = "true"; 122 | return getPort({ port: this.debugPort }).then((port) => { 123 | if (this.config.getProject().isProjectLanguage(GaugeRunners.Dotnet)) { 124 | env.GAUGE_CSHARP_PROJECT_CONFIG = "Debug"; 125 | } 126 | if (this.config.getProject().isProjectLanguage(GaugeRunners.Java)) env.GAUGE_DEBUG_OPTS = port; 127 | env.DEBUG_PORT = port; 128 | this.debugPort = port; 129 | return env; 130 | }); 131 | } else { 132 | return Promise.resolve(env); 133 | } 134 | } 135 | 136 | public startDebugger() { 137 | return new Promise((res, rej) => { 138 | const root = this.config.getProject().root(); 139 | const folder = workspace.getWorkspaceFolder(Uri.file(root)); 140 | if (!folder) { 141 | throw new Error(`The debugger does not work for a stand alone file. Please open the folder ${root}.`); 142 | } 143 | setTimeout(() => { 144 | debug.startDebugging(folder, this.getDebuggerConf()).then(res, rej); 145 | }, 100); 146 | }); 147 | } 148 | 149 | public registerStopDebugger(callback) { 150 | debug.onDidTerminateDebugSession((e: DebugSession) => { 151 | callback(e); 152 | }); 153 | } 154 | 155 | public stopDebugger(): void { 156 | if (debug.activeDebugSession) { 157 | debug.activeDebugSession.customRequest("disconnect"); 158 | } 159 | } 160 | 161 | } 162 | 163 | interface ConfigObj { 164 | name: string; 165 | type: string; 166 | request: string; 167 | processId: number; 168 | justMyCode: boolean; 169 | sourceFileMap: { [sourceFile: string]: string }; 170 | } 171 | -------------------------------------------------------------------------------- /test/execution/runArgs.test.ts: -------------------------------------------------------------------------------- 1 | import assert = require('assert'); 2 | import { buildRunArgs, extractGaugeRunOption } from '../../src/execution/runArgs'; 3 | 4 | suite('Gauge Run Args Tests', () => { 5 | suite('buildRunArgs.forGauge', () => { 6 | test('should ignore other args when failed flag is set', () => { 7 | assert.strictEqual( 8 | buildRunArgs.forGauge('my.spec:123', { failed: true, tags: 'should be ignored' }).join(' '), 9 | 'run --failed'); 10 | }); 11 | 12 | test('should ignore other args when repeat flag is set', () => { 13 | assert.strictEqual( 14 | buildRunArgs.forGauge('my.spec:123', { repeat: true, tags: 'should be ignored' }).join(' '), 15 | 'run --repeat'); 16 | }); 17 | 18 | test('should be formatted', () => { 19 | assert.strictEqual( 20 | buildRunArgs.forGauge('my.spec:123', { 21 | tags: 'foo bar', 22 | n: 3, 23 | env: ['a', 'b', 'c'], 24 | parallel: true, 25 | // following should be ignored 26 | failed: null, 27 | repeat: false, 28 | 'retry-only': null 29 | }).join(' '), 30 | 'run --hide-suggestion --tags foo bar --n 3 --env a,b,c --parallel my.spec:123'); 31 | }); 32 | 33 | test('should not contain simple-console flag when parallel flag is set', () => { 34 | assert.strictEqual( 35 | buildRunArgs.forGauge('my.spec:123', { parallel: true }).join(' '), 36 | 'run --hide-suggestion --parallel my.spec:123'); 37 | }); 38 | 39 | test('should be unsettable', () => { 40 | assert.strictEqual( 41 | buildRunArgs.forGauge(null, { 'hide-suggestion': false, 'simple-console': false }).join(' '), 42 | 'run'); 43 | }); 44 | }); 45 | 46 | suite('buildRunArgs.forGradle', () => { 47 | test('should ignore other args when failed flag is set', () => { 48 | assert.strictEqual( 49 | buildRunArgs.forGradle('my.spec:123', { failed: true }).join(' '), 50 | 'clean gauge -PadditionalFlags=--failed'); 51 | }); 52 | 53 | test('should ignore other args when repeat flag is set', () => { 54 | assert.strictEqual( 55 | buildRunArgs.forGradle('my.spec:123', { repeat: true }).join(' '), 56 | 'clean gauge -PadditionalFlags=--repeat'); 57 | }); 58 | 59 | test('should be formatted', () => { 60 | assert.strictEqual( 61 | buildRunArgs.forGradle('my.spec:123', { 62 | tags: 'foo bar', 63 | env: ['a', 'b', 'c'], 64 | parallel: true, 65 | n: 3, 66 | // following should be ignored 67 | failed: null, 68 | repeat: false, 69 | 'retry-only': null 70 | }).join(' '), 71 | 'clean gauge -PinParallel=true -Pnodes=3 -Ptags=foo bar -Penv=a,b,c -PadditionalFlags=--hide-suggestion --simple-console -PspecsDir=my.spec:123'); 72 | }); 73 | 74 | test('should be unsettable', () => { 75 | assert.strictEqual( 76 | buildRunArgs.forGradle(null, { 'hide-suggestion': false, 'simple-console': false }).join(' '), 77 | 'clean gauge'); 78 | }); 79 | }); 80 | 81 | suite('buildRunArgs.forMaven', () => { 82 | test('should ignore other args when failed flag is set', () => { 83 | assert.strictEqual( 84 | buildRunArgs.forMaven('my.spec:123', { failed: true }).join(' '), 85 | '-q clean compile test-compile gauge:execute -Dflags=--failed'); 86 | }); 87 | 88 | test('should ignore other args when repeat flag is set', () => { 89 | assert.strictEqual( 90 | buildRunArgs.forMaven('my.spec:123', { repeat: true }).join(' '), 91 | '-q clean compile test-compile gauge:execute -Dflags=--repeat'); 92 | }); 93 | 94 | test('should be formatted', () => { 95 | assert.strictEqual( 96 | buildRunArgs.forMaven('my.spec:123', { 97 | tags: 'foo bar', 98 | env: ['a', 'b', 'c'], 99 | parallel: true, 100 | n: 3, 101 | // following should be ignored 102 | failed: null, 103 | repeat: false, 104 | 'retry-only': null 105 | }).join(' '), 106 | '-q clean compile test-compile gauge:execute -DinParallel=true -Dnodes=3 -Dtags=foo bar -Denv=a,b,c -Dflags=--hide-suggestion,--simple-console -DspecsDir=my.spec:123'); 107 | }); 108 | 109 | test('should be unsettable', () => { 110 | assert.strictEqual( 111 | buildRunArgs.forMaven(null, { 'hide-suggestion': false, 'simple-console': false }).join(' '), 112 | '-q clean compile test-compile gauge:execute'); 113 | }); 114 | 115 | }); 116 | 117 | suite('extractGaugeRunOption', () => { 118 | test('should pick the first gauge:test entry and remove the common launch attributes', () => { 119 | const configs = [ 120 | { type: 'foo', name: '1', request: 'launch', tags: 'fail' }, 121 | { type: 'bar', name: '2', request: 'test', tags: 'fail' }, 122 | { type: 'gauge', name: '3', request: 'attach', tags: 'fail' }, 123 | { type: 'gauge', name: '4', request: 'test', tags: 'hit', unknown: 'attributes are also available' }, 124 | { type: 'gauge', name: '5', request: 'test', tags: 'fail' } 125 | ] 126 | assert.deepStrictEqual(extractGaugeRunOption(configs), { tags: 'hit', unknown: 'attributes are also available' }); 127 | }); 128 | 129 | test('should return empty object when no gauge:test entry is found', () => { 130 | const configs = [ 131 | { type: 'foo', name: '1', request: 'launch' }, 132 | { type: 'bar', name: '2', request: 'test' }, 133 | { type: 'gauge', name: '3', request: 'attach' }, 134 | ] 135 | assert.deepStrictEqual(extractGaugeRunOption(configs), {}); 136 | }); 137 | 138 | test('should return empty object for null', () => { 139 | assert.deepStrictEqual(extractGaugeRunOption(null), {}); 140 | }); 141 | }); 142 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gauge extension for [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=getgauge.gauge) 2 | 3 | # Install 4 | 5 | ``` 6 | $ code --install-extension getgauge.gauge 7 | ``` 8 | *Other Install [options](#install-from-source)* 9 | 10 | 11 | # Features 12 | 13 | * [Create New Project](#create-new-project) 14 | * [Code Completion](#code-completion) 15 | * [Goto Definition](#goto-definition) 16 | * [Diagnostics](#diagnostics) 17 | * [Format Specifications](#format-specifications) 18 | * [References](#references) 19 | * [Symbols](#symbols) 20 | * [Run Specs/Scenarios](#run-specifications-and-scenarios) 21 | * [Debug Specs/Scenarios](#debug-specifications-and-scenarios) 22 | * [Reports](#reports) 23 | * [Test Explorer](#test-explorer) 24 | * [Code Snippets](#snippets-for-specification-scenarios-and-tables) 25 | 26 | Gauge language plugins supported by the Gauge Visual Studio Code plugin are: 27 | * [gauge-js](https://github.com/getgauge/gauge-js) 28 | * [gauge-java](https://github.com/getgauge/gauge-java) 29 | * [gauge-dotnet](https://github.com/getgauge/gauge-dotnet) 30 | * [gauge-python](https://github.com/getgauge/gauge-python) 31 | * [gauge-ruby](https://github.com/getgauge/gauge-ruby) 32 | 33 | ## Create new project 34 | 35 | Execute the Command `Gauge: Create new Gauge Project` and select the appropriate template to create a new Gauge Project 36 | 37 | Create New Project preview 38 | 39 | ## Code Completion 40 | Code Completion preview 41 | 42 | ## Goto Definition 43 | Goto Definition preview 44 | 45 | ## Diagnostics 46 | 47 | Diagnostics preview 48 | 49 | ## Format Specifications 50 | 51 | Formatting preview 52 | 53 | ## Symbols 54 | 55 | Symbols preview 56 | 57 | ## References 58 | 59 | References preview 60 | 61 | ## Run specifications and scenarios 62 | 63 | ### Using Codelens 64 | 65 | Run Specs/Scenarios preview 66 | 67 | ### Using command palette 68 | [Launch the command palette](https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line) 69 | 70 | * Gauge: Create a new Gauge Project 71 | * Gauge: Create a new Specification 72 | * Gauge: Find Step References 73 | * Gauge: Optimize VS Code Configuration for Gauge 74 | * Gauge: Run All Specification 75 | * Gauge: Run Specification 76 | * Gauge: Run Scenarios 77 | * Gauge: Run Scenario At Cursor 78 | * Gauge: Repeat Last Run 79 | * Gauge: Re-Run Failed Scenario(s) 80 | * Gauge: Show Last Run Report 81 | * Gauge: Stop current gauge execution 82 | * Gauge: Report Issue 83 | * Test: Focus on Gauge Specs View 84 | 85 | ## Debug specifications and scenarios 86 | 87 | Debug Specs/Scenarios preview 88 | 89 | Suport for Debugging of step implementations in JS, Python and Ruby 90 | 91 | ## Reports 92 | 93 | View execution reports inside VS Code 94 | 95 | Execution Report preview 96 | 97 | ## Test Explorer 98 | 99 | Test Explorer preview 100 | 101 | ## Snippets for specification, scenarios and tables 102 | 103 | To invoke a snippet type any of the following snippet keywords and Ctrl+space 104 | 105 | * `spec` - for specification 106 | * `sce` - for scenario 107 | * `table:1` - table with one column 108 | * `table:2` - table with two columns 109 | * `table:3` - table with three columns 110 | * `table:4` - table with four columns 111 | * `table:5` - table with five columns 112 | * `table:6` - table with six columns 113 | 114 | # Configuration 115 | 116 | To override default configurations in [VSCode settings](https://code.visualstudio.com/docs/getstarted/settings) 117 | 118 | * `gauge.launch.enableDebugLogs`: Starts gauge lsp server with log-level `debug`, defaults to `false` 119 | * `gauge.execution.debugPort`: Debug port, defaults to `9229` 120 | * `gauge.notification.suppressUpdateNotification`: Stops notifications for gauge-vscode plugin auto-updates, defaults to `false` 121 | * `gauge.create.specification.withHelp`: Create specification template with help comments, defaults to `true` 122 | 123 | ## Run and Debug configuration 124 | 125 | To specify the execution options for the tests, add a `"type":"gauge"` entry with `"request":"test"` to `launch.json`. 126 | The options of the [gage run command](https://manpage.gauge.org/gauge_run.html) are available as properties of the entry. For Maven and Gradle plugins, the corresponding arguments are supported. 127 | If there are multiple `"request":"test"` entries in `launch.json`, the first definition will be used. 128 | 129 | # Install from source 130 | 131 | $ npm run build 132 | 133 | This will create `gauge-.vsix` file which can be installed via VScode's [Install from VSIX](https://code.visualstudio.com/docs/editor/extension-gallery#_install-from-a-vsix). 134 | > Note: Manually delete the Gauge extension folder from [VSCode extensions folder](https://vscode-docs.readthedocs.io/en/stable/extensions/install-extension/) for a successful uninstallation of VSCode extension 135 | 136 | # Troubleshooting 137 | 138 | If gauge features are not activated, check file associations for `.spec` and `.cpt` it maybe used by another plugin. To fix this, add this to [user settings](https://code.visualstudio.com/docs/getstarted/settings) 139 | 140 | ``` 141 | "files.associations": { 142 | "*.spec": "gauge", 143 | "*.cpt": "gauge" 144 | } 145 | ``` 146 | 147 | ## Facing other issues? 148 | 149 | Refer our [Troubleshooting](https://docs.getgauge.io/troubleshooting.html) guide 150 | 151 | # Talk to us 152 | 153 | Please see below for the best place to ask a query: 154 | 155 | - How do I? -- [Stack Overflow](https://stackoverflow.com/questions/ask?tags=getgauge) 156 | - I got this error, why? -- [Stack Overflow](https://stackoverflow.com/questions/ask?tags=getgauge) 157 | - I got this error and I'm sure it's a bug -- file an [issue](https://github.com/getgauge/gauge-vscode/issues) 158 | - I have an idea/request -- file an [issue](https://github.com/getgauge/gauge-vscode/issues) 159 | - Why do you? -- [Google Groups](https://groups.google.com/forum/#!forum/getgauge) 160 | - When will you? -- [Google Groups](https://groups.google.com/forum/#!forum/getgauge) 161 | -------------------------------------------------------------------------------- /test/cli.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { CLI, Command } from '../src/cli'; 3 | import path = require('path'); 4 | import { spawnSync } from "child_process"; 5 | 6 | let testCommandsPath = path.join(__dirname, '..', '..', 'test', 'commands'); 7 | 8 | suite('CLI', () => { 9 | test('.isPluginInstalled should tell a gauge plugin is installed or not', () => { 10 | let info = { 11 | version: "1.2.3", 12 | plugins: [ 13 | { name: "csharp", version: "1.2.0" }, 14 | { name: "java", version: "1.0.0" }, 15 | { name: "ruby", version: "1.2.0" }, 16 | ] 17 | }; 18 | 19 | let cli = new CLI(new Command("gauge"), info, new Command('mvn'), new Command('gradle')); 20 | assert.ok(cli.isPluginInstalled('java')); 21 | assert.notEqual(true, cli.isPluginInstalled('foo')); 22 | }); 23 | 24 | test('.getPluginVersion should give version of given plugin', () => { 25 | let info = { 26 | version: "1.2.3", 27 | plugins: [ 28 | { name: "csharp", version: "1.2.0" }, 29 | { name: "java", version: "1.0.0" }, 30 | { name: "ruby", version: "1.2.0" }, 31 | ] 32 | }; 33 | 34 | let cli = new CLI(new Command("gauge"), info, new Command('mvn'), new Command('gradle')); 35 | assert.equal('1.0.0', cli.getGaugePluginVersion('java')); 36 | }); 37 | 38 | test('.isPluginInstalled should tell a gauge plugin is installed or not', () => { 39 | let info = { 40 | version: "1.2.3", 41 | commitHash: "3db28e6", 42 | plugins: [ 43 | { name: "csharp", version: "1.2.0" }, 44 | { name: "java", version: "1.0.0" }, 45 | { name: "ruby", version: "1.2.0" }, 46 | ] 47 | }; 48 | 49 | let cli = new CLI(new Command("gauge"), info, new Command('mvn'), new Command('gradle')); 50 | assert.ok(cli.isPluginInstalled('java')); 51 | assert.notEqual(true, cli.isPluginInstalled('foo')); 52 | }); 53 | 54 | test('.versionString should give its version information as string', (done) => { 55 | let info = { 56 | version: "1.2.3", 57 | commitHash: "3db28e6", 58 | plugins: [ 59 | { name: "csharp", version: "1.2.0" }, 60 | { name: "java", version: "1.0.0" }, 61 | { name: "ruby", version: "1.2.0" }, 62 | ] 63 | }; 64 | 65 | let cli = new CLI(new Command("gauge"), info, new Command('mvn'), new Command('gradle')); 66 | 67 | let expected = `Gauge version: 1.2.3 68 | Commit Hash: 3db28e6 69 | 70 | Plugins 71 | ------- 72 | csharp (1.2.0) 73 | java (1.0.0) 74 | ruby (1.2.0)`; 75 | 76 | assert.equal(cli.gaugeVersionString(), expected); 77 | done(); 78 | }); 79 | 80 | test('.versionString should not give commit hash if not available', (done) => { 81 | let info = { 82 | version: "1.2.3", 83 | plugins: [ 84 | { name: "csharp", version: "1.2.0" }, 85 | { name: "java", version: "1.0.0" }, 86 | { name: "ruby", version: "1.2.0" }, 87 | ] 88 | }; 89 | 90 | let cli = new CLI(new Command("gauge"), info, new Command('mvn'), new Command('gradle')); 91 | 92 | let expected = `Gauge version: 1.2.3 93 | 94 | 95 | Plugins 96 | ------- 97 | csharp (1.2.0) 98 | java (1.0.0) 99 | ruby (1.2.0)`; 100 | 101 | assert.equal(cli.gaugeVersionString(), expected); 102 | done(); 103 | }); 104 | 105 | test('.isVersionGreaterOrEqual should tell if version is greater than or' + 106 | 'equal with minimum supported gauge version', (done) => { 107 | let info = { 108 | version: "1.2.3", 109 | commitHash: "3db28e6", 110 | plugins: [ 111 | { name: "csharp", version: "1.2.0" }, 112 | { name: "java", version: "1.0.0" }, 113 | { name: "ruby", version: "1.2.0" }, 114 | ] 115 | }; 116 | 117 | let cli = new CLI(new Command("gauge"), info, new Command('mvn'), new Command('gradle')); 118 | 119 | assert.ok(cli.isGaugeVersionGreaterOrEqual('1.2.3')); 120 | assert.ok(cli.isGaugeVersionGreaterOrEqual('1.2.0')); 121 | assert.ok(cli.isGaugeVersionGreaterOrEqual('0.9.0')); 122 | assert.ok(cli.isGaugeVersionGreaterOrEqual('0.2.8')); 123 | 124 | done(); 125 | }); 126 | 127 | test('.isVersionGreaterOrEqual should tell if version is lower than minimum supported gauge version', (done) => { 128 | let info = { 129 | version: "1.2.3", 130 | commitHash: "3db28e6", 131 | plugins: [ 132 | { name: "csharp", version: "1.2.0" }, 133 | { name: "java", version: "1.0.0" }, 134 | { name: "ruby", version: "1.2.0" }, 135 | ] 136 | }; 137 | let cli = new CLI(new Command("gauge"), info, new Command('mvn'), new Command('gradle')); 138 | 139 | assert.ok(!cli.isGaugeVersionGreaterOrEqual('2.0.0')); 140 | assert.ok(!cli.isGaugeVersionGreaterOrEqual('2.1.3')); 141 | assert.ok(!cli.isGaugeVersionGreaterOrEqual('1.3.0')); 142 | done(); 143 | }); 144 | 145 | test('.isGaugeInstalled should tell if gauge is installed or not', (done) => { 146 | assert.ok(new CLI(new Command("gauge"), {}, undefined, undefined).isGaugeInstalled()); 147 | assert.ok(!new CLI(null, {}, undefined, undefined).isGaugeInstalled()); 148 | done(); 149 | }); 150 | 151 | test('.getCommandCandidates choices all valid by .isSpawnable', (done) => { 152 | let candidates = CLI.getCommandCandidates('test_command'); 153 | const originalPath = process.env.PATH; 154 | process.env.PATH = testCommandsPath; 155 | let invalid_candidates = []; 156 | try { 157 | for (const candidate of candidates) { 158 | if (!CLI.isSpawnable(candidate)) { 159 | invalid_candidates.push(candidate); 160 | } 161 | } 162 | assert.ok(invalid_candidates.length === 0, `invalid candidates: ${invalid_candidates.join(', ')}, those should be valid`); 163 | } finally { 164 | process.env.PATH = originalPath; 165 | } 166 | done(); 167 | }); 168 | 169 | test('.getCommandCandidates choices can be found as in valid via .isSpawnable', (done) => { 170 | let candidates = CLI.getCommandCandidates('test_command_not_found'); 171 | const originalPath = process.env.PATH; 172 | process.env.PATH = testCommandsPath; 173 | let valid_candidates = []; 174 | try { 175 | for (const candidate of candidates) { 176 | if (CLI.isSpawnable(candidate)) { 177 | valid_candidates.push(candidate); 178 | } 179 | } 180 | assert.ok(valid_candidates.length === 0, `valid candidates: ${valid_candidates.join(', ')}, those should not be valid`); 181 | } finally { 182 | process.env.PATH = originalPath; 183 | } 184 | done(); 185 | }); 186 | 187 | test('.getCommandCandidates can be spawned with an arg', (done) => { 188 | let candidates = CLI.getCommandCandidates('test_command'); 189 | const originalPath = process.env.PATH; 190 | process.env.PATH = testCommandsPath; 191 | try { 192 | for (const candidate of candidates.filter(c => (c.cmdSuffix !== ".exe"))) { 193 | const result = candidate.spawnSync(["Hello World"]); 194 | assert.ok(result.status === 0 && !result.error, `Command candidate ${candidate.command} failed to spawn`); 195 | assert.equal(result.stdout.toString().trim(), 'Success: "Hello World"', `Command candidate ${candidate.command} has wrong output`) 196 | } 197 | } finally { 198 | process.env.PATH = originalPath; 199 | } 200 | done(); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /src/semanticTokensProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | const tokenTypes = [ 4 | 'specification', // For spec/concept headers: lines starting with '#' or underlined with '=' 5 | 'scenario', // For scenario headers: lines starting with '##' or underlined with '-' 6 | 'stepMarker', // For the leading "*" in a step line 7 | 'step', // For the rest of the step text 8 | 'argument', // For any quoted text or angle-bracketed text in step lines (or concept heading) 9 | 'table', // For table cell text (non-border, non-separator) 10 | 'tableHeaderSeparator', // For table header separator dash characters (only '-' characters) 11 | 'tableBorder', // For table border characters (the '|' characters) 12 | 'tagKeyword', // For the literal "tags:" at the beginning of a tag line 13 | 'tagValue', // For the remainder of a tag line after "tags:" 14 | 'disabledStep', // For lines starting with "//" (used to disable a step) 15 | 'gaugeComment' // For lines that do not match any of the above (fallback comment lines) 16 | ]; 17 | const tokenModifiers: string[] = []; 18 | export const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers); 19 | 20 | export class GaugeSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider { 21 | public provideDocumentSemanticTokens( 22 | document: vscode.TextDocument, 23 | token: vscode.CancellationToken 24 | ): vscode.ProviderResult { 25 | const builder = new vscode.SemanticTokensBuilder(legend); 26 | const lines = document.getText().split(/\r?\n/); 27 | 28 | // Combined regular expression to match text within double quotes OR within angle brackets. 29 | const argumentRegex = /(?:"([^"]+)"|<([^>]+)>)/g; 30 | // Regular expression to detect a table header separator line. 31 | // Matches lines that start and end with a pipe and contain only dashes, pipes, and optional spaces. 32 | const tableHeaderSeparatorRegex = /^\|\s*-+\s*(\|\s*-+\s*)+\|?$/; 33 | 34 | // Process the document with a manual loop (to allow multi‑line heading handling). 35 | for (let i = 0; i < lines.length;) { 36 | const line = lines[i]; 37 | const trimmedLine = line.trim(); 38 | 39 | // 0. Check for comment lines - lines starting with "//" 40 | if (trimmedLine.startsWith("//")) { 41 | builder.push(i, 0, line.length, tokenTypes.indexOf('disabledStep'), 0); 42 | i++; 43 | continue; 44 | } 45 | 46 | // 1. Check for underlined headings: 47 | // If the next line exists and is made up entirely of "=" then this is a specification header. 48 | if (i + 1 < lines.length) { 49 | const nextLine = lines[i + 1]; 50 | const trimmedNextLine = nextLine.trim(); 51 | if (trimmedNextLine.length > 0 && /^[=]+$/.test(trimmedNextLine)) { 52 | const leadingSpaces = line.length - line.trimStart().length; 53 | // Mark the heading line. 54 | builder.push(i, leadingSpaces, line.length - leadingSpaces, tokenTypes.indexOf('specification'), 0); 55 | // Mark the underline line. 56 | builder.push(i + 1, 0, nextLine.length, tokenTypes.indexOf('specification'), 0); 57 | i += 2; 58 | continue; 59 | } 60 | // If the next line is made up entirely of "-" then this is a scenario header. 61 | if (trimmedNextLine.length > 0 && /^[-]+$/.test(trimmedNextLine)) { 62 | const leadingSpaces = line.length - line.trimStart().length; 63 | builder.push(i, leadingSpaces, line.length - leadingSpaces, tokenTypes.indexOf('scenario'), 0); 64 | builder.push(i + 1, 0, nextLine.length, tokenTypes.indexOf('scenario'), 0); 65 | i += 2; 66 | continue; 67 | } 68 | } 69 | 70 | // 2. Check for '#' style headings. 71 | // Now we process any heading line that starts with '#' to also highlight arguments. 72 | if (trimmedLine.startsWith("#")) { 73 | // Find the first non‑whitespace index. 74 | const firstNonWhitespaceIndex = line.search(/\S/); 75 | let lastIndex = firstNonWhitespaceIndex; 76 | // Reset regex state for this line. 77 | argumentRegex.lastIndex = 0; 78 | let match: RegExpExecArray | null = argumentRegex.exec(line); 79 | while (match !== null) { 80 | const matchStart = match.index; 81 | if (matchStart > lastIndex) { 82 | // Mark the non‑argument portion as a scenario token. 83 | builder.push(i, lastIndex, matchStart - lastIndex, tokenTypes.indexOf('scenario'), 0); 84 | } 85 | // Mark the matched argument text as an argument token. 86 | builder.push(i, matchStart, match[0].length, tokenTypes.indexOf('argument'), 0); 87 | lastIndex = argumentRegex.lastIndex; 88 | match = argumentRegex.exec(line); 89 | } 90 | if (lastIndex < line.length) { 91 | builder.push(i, lastIndex, line.length - lastIndex, tokenTypes.indexOf('scenario'), 0); 92 | } 93 | i++; 94 | continue; 95 | } 96 | 97 | // 3. Check for tag lines (lines starting with "tags:" case-insensitively). 98 | else if (trimmedLine.toLowerCase().startsWith('tags:')) { 99 | const leadingSpaces = line.length - line.trimStart().length; 100 | const keyword = "tags:"; 101 | builder.push(i, leadingSpaces, keyword.length, tokenTypes.indexOf('tagKeyword'), 0); 102 | const tagValueStart = leadingSpaces + keyword.length; 103 | if (tagValueStart < line.length) { 104 | builder.push(i, tagValueStart, line.length - tagValueStart, tokenTypes.indexOf('tagValue'), 0); 105 | } 106 | i++; 107 | continue; 108 | } 109 | 110 | // 4. Process step lines (lines starting with '*'). 111 | else if (trimmedLine.startsWith('*')) { 112 | const firstNonWhitespaceIndex = line.indexOf('*'); 113 | if (firstNonWhitespaceIndex !== -1) { 114 | // Mark the "*" as a stepMarker. 115 | builder.push(i, firstNonWhitespaceIndex, 1, tokenTypes.indexOf('stepMarker'), 0); 116 | let lastIndex = firstNonWhitespaceIndex + 1; 117 | // Reset regex state for this line. 118 | argumentRegex.lastIndex = 0; 119 | let match: RegExpExecArray | null = argumentRegex.exec(line); 120 | while (match !== null) { 121 | const matchStart = match.index; 122 | if (matchStart > lastIndex) { 123 | builder.push(i, lastIndex, matchStart - lastIndex, tokenTypes.indexOf('step'), 0); 124 | } 125 | // Mark the entire matched text (including quotes or angle brackets) as an argument. 126 | builder.push(i, matchStart, match[0].length, tokenTypes.indexOf('argument'), 0); 127 | lastIndex = argumentRegex.lastIndex; 128 | match = argumentRegex.exec(line); 129 | } 130 | // Any remaining text after the last argument is part of the step. 131 | if (lastIndex < line.length) { 132 | builder.push(i, lastIndex, line.length - lastIndex, tokenTypes.indexOf('step'), 0); 133 | } 134 | } 135 | i++; 136 | continue; 137 | } 138 | 139 | // 5. Process table lines (lines starting with '|'). 140 | else if (trimmedLine.startsWith('|')) { 141 | if (tableHeaderSeparatorRegex.test(trimmedLine)) { 142 | // Process the table separator line character-by-character. 143 | for (let j = 0; j < line.length; j++) { 144 | const char = line[j]; 145 | if (char === '|') { 146 | // Mark pipe characters as tableBorder. 147 | builder.push(i, j, 1, tokenTypes.indexOf('tableBorder'), 0); 148 | } else if (char === '-') { 149 | let start = j; 150 | while (j < line.length && line[j] === '-') { 151 | j++; 152 | } 153 | // Group consecutive dashes as tableHeaderSeparator. 154 | builder.push(i, start, j - start, tokenTypes.indexOf('tableHeaderSeparator'), 0); 155 | j--; // adjust for outer loop increment 156 | } else { 157 | // Other characters (likely whitespace) get the "table" token. 158 | builder.push(i, j, 1, tokenTypes.indexOf('table'), 0); 159 | } 160 | } 161 | } else { 162 | // Process a normal table row character-by-character. 163 | for (let j = 0; j < line.length; j++) { 164 | const char = line[j]; 165 | if (char === '|') { 166 | builder.push(i, j, 1, tokenTypes.indexOf('tableBorder'), 0); 167 | } else { 168 | builder.push(i, j, 1, tokenTypes.indexOf('table'), 0); 169 | } 170 | } 171 | } 172 | i++; 173 | continue; 174 | } 175 | else { 176 | // For any other non-empty line, mark it as a comment. 177 | if (trimmedLine.length > 0) { 178 | builder.push(i, 0, line.length, tokenTypes.indexOf('gaugeComment'), 0); 179 | } 180 | i++; 181 | } 182 | } 183 | return builder.build(); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/explorer/specExplorer.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as path from 'path'; 4 | import * as vscode from 'vscode'; 5 | import { 6 | commands, Disposable, Position, Range, TextDocument, TextDocumentShowOptions, 7 | TextEditor, Uri, window, workspace 8 | } from 'vscode'; 9 | import { LanguageClient, TextDocumentIdentifier } from 'vscode-languageclient/node'; 10 | import { GaugeCommandContext, GaugeRequests, GaugeVSCodeCommands, setCommandContext } from '../constants'; 11 | import { ExecutionConfig } from '../execution/executionConfig'; 12 | import { GaugeWorkspace } from '../gaugeWorkspace'; 13 | 14 | const extensions = [".spec", ".md"]; 15 | 16 | export class SpecNodeProvider extends Disposable implements vscode.TreeDataProvider { 17 | private _onDidChangeTreeData: vscode.EventEmitter = 18 | new vscode.EventEmitter(); 19 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 20 | private activeFolder: string; 21 | private _disposable: Disposable; 22 | private _languageClient?: LanguageClient; 23 | 24 | constructor(private gaugeWorkspace: GaugeWorkspace) { 25 | super(() => this.dispose()); 26 | setCommandContext(GaugeCommandContext.Activated, false); 27 | if (isSpecExplorerEnabled()) { 28 | const disposable = window.registerTreeDataProvider(GaugeCommandContext.GaugeSpecExplorer, this); 29 | this.activeFolder = gaugeWorkspace.getDefaultFolder(); 30 | this.activateTreeDataProvider(this.activeFolder); 31 | const refreshMethod = (fileUri: vscode.Uri) => { 32 | if (this.shouldRefresh(fileUri)) { 33 | this.refresh(); 34 | } 35 | }; 36 | vscode.workspace.onDidSaveTextDocument((doc: vscode.TextDocument) => { 37 | refreshMethod(doc.uri); 38 | }); 39 | workspace.onDidCloseTextDocument((doc: TextDocument) => { 40 | refreshMethod(doc.uri); 41 | }); 42 | 43 | const watcher = workspace.createFileSystemWatcher("**/*.{spec,md}", true, false, true); 44 | watcher.onDidCreate(refreshMethod); 45 | watcher.onDidDelete(refreshMethod); 46 | 47 | this._disposable = Disposable.from(disposable, watcher, 48 | commands.registerCommand(GaugeVSCodeCommands.SwitchProject, 49 | () => gaugeWorkspace.showProjectOptions((path: string) => { 50 | this.changeClient(path); 51 | }) 52 | ), 53 | commands.registerCommand(GaugeVSCodeCommands.ExecuteAllSpecExplorer, () => { 54 | return this.gaugeWorkspace.getGaugeExecutor().runSpecification(this.activeFolder); 55 | }), 56 | commands.registerCommand(GaugeVSCodeCommands.ExecuteScenario, (scn: Scenario) => { 57 | if (scn) return this.gaugeWorkspace.getGaugeExecutor().execute(scn.executionIdentifier, 58 | new ExecutionConfig().setStatus(scn.executionIdentifier) 59 | .setProject(this.gaugeWorkspace.getClientsMap().get(scn.file).project)); 60 | return this.gaugeWorkspace.getGaugeExecutor().runScenario(true); 61 | }), 62 | commands.registerCommand(GaugeVSCodeCommands.ExecuteSpec, (spec: Spec) => { 63 | if (spec) { 64 | return this.gaugeWorkspace.getGaugeExecutor().execute(spec.file, 65 | new ExecutionConfig().setStatus(spec.file) 66 | .setProject(this.gaugeWorkspace.getClientsMap().get(spec.file).project)); 67 | } 68 | return this.gaugeWorkspace.getGaugeExecutor().runSpecification(); 69 | }), 70 | commands.registerCommand(GaugeVSCodeCommands.Open, 71 | (node: GaugeNode) => workspace.openTextDocument(node.file) 72 | .then(this.showDocumentWithSelection(node))), 73 | 74 | commands.registerCommand(GaugeVSCodeCommands.ExecuteNode, (node: GaugeNode) => 75 | this.gaugeWorkspace.getGaugeExecutor().execute( 76 | node instanceof Scenario ? node.executionIdentifier : node.file, 77 | new ExecutionConfig().setStatus(node.file) 78 | .setProject(this.gaugeWorkspace.getClientsMap().get(node.file).project)) 79 | ), 80 | 81 | commands.registerCommand(GaugeVSCodeCommands.DebugNode, (node: GaugeNode) => 82 | this.gaugeWorkspace.getGaugeExecutor().execute( 83 | node instanceof Scenario ? node.executionIdentifier : node.file, 84 | new ExecutionConfig().setStatus(node.file).setDebug() 85 | .setProject(this.gaugeWorkspace.getClientsMap().get(node.file).project)) 86 | ) 87 | ); 88 | } 89 | } 90 | 91 | refresh(element?: GaugeNode): void { 92 | this._onDidChangeTreeData.fire(element); 93 | } 94 | 95 | getTreeItem(element: GaugeNode): vscode.TreeItem { 96 | return element; 97 | } 98 | 99 | getChildren(element?: GaugeNode): Thenable { 100 | if (!this.activeFolder) { 101 | vscode.window.showInformationMessage('No dependency in empty workspace'); 102 | return Promise.resolve([]); 103 | } 104 | if (!this._languageClient) return Promise.resolve([]); 105 | 106 | return new Promise((resolve, reject) => { 107 | if (element && element.contextValue === "specification") { 108 | let uri = TextDocumentIdentifier.create(element.file); 109 | return this._languageClient.sendRequest(GaugeRequests.Scenarios, { 110 | textDocument: uri, 111 | position: new vscode.Position(1, 1) 112 | }, new vscode.CancellationTokenSource().token).then( 113 | (val: any[]) => { 114 | resolve(val.map((x) => { 115 | const specFile = x.executionIdentifier.split(":" + x.lineNo)[0]; 116 | return new Scenario(x.heading, specFile, x.lineNo); 117 | })); 118 | }, 119 | (reason) => { console.log(reason); reject(reason); } 120 | ); 121 | } else { 122 | let token = new vscode.CancellationTokenSource().token; 123 | return this._languageClient.sendRequest(GaugeRequests.Specs, {}, token) 124 | .then( 125 | (val: any[]) => { 126 | resolve(val.map((x) => { 127 | if (x.heading) { 128 | return new Spec(x.heading, x.executionIdentifier); 129 | } 130 | })); 131 | } 132 | ); 133 | } 134 | }); 135 | } 136 | 137 | private shouldRefresh(fileUri: vscode.Uri): boolean { 138 | return extensions.includes(path.extname(fileUri.fsPath)) && 139 | this.gaugeWorkspace.getClientsMap().get(fileUri.fsPath).project.root() === this.activeFolder; 140 | } 141 | 142 | changeClient(projectPath: string) { 143 | setCommandContext(GaugeCommandContext.Activated, false); 144 | if (isSpecExplorerEnabled()) { 145 | this.activateTreeDataProvider(projectPath); 146 | } 147 | } 148 | 149 | private activateTreeDataProvider(projectPath: string) { 150 | if (!projectPath) return; 151 | const workspacePath = Uri.file(projectPath).fsPath; 152 | const client = this.gaugeWorkspace.getClientsMap().get(workspacePath).client; 153 | if (!client) return; 154 | client.start().then(() => { 155 | this._languageClient = client; 156 | this.activeFolder = projectPath; 157 | this.refresh(); 158 | setTimeout(setCommandContext, 1000, GaugeCommandContext.Activated, true); 159 | }).catch((reason) => { 160 | window.showErrorMessage("Failed to create test explorer.", reason); 161 | }); 162 | } 163 | 164 | private showDocumentWithSelection(node: GaugeNode): (value: TextDocument) => TextEditor | Thenable { 165 | return (document) => { 166 | if (node instanceof Scenario) { 167 | let scenarioNode: Scenario = node; 168 | let options: TextDocumentShowOptions = { 169 | selection: new Range(new Position(scenarioNode.lineNo - 1, 0), 170 | new Position(scenarioNode.lineNo - 1, 0)) 171 | }; 172 | return window.showTextDocument(document, options); 173 | } 174 | if (node instanceof Spec) { 175 | let options: TextDocumentShowOptions = { 176 | selection: new Range(new Position(0, 0), new Position(0, 0)) 177 | }; 178 | return window.showTextDocument(document, options); 179 | } 180 | return window.showTextDocument(document); 181 | }; 182 | } 183 | } 184 | 185 | export abstract class GaugeNode extends vscode.TreeItem { 186 | constructor( 187 | public readonly label: string, 188 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 189 | public readonly file: string 190 | ) { 191 | super(label, vscode.TreeItemCollapsibleState.Collapsed); 192 | } 193 | command = { title: 'Open File', command: GaugeVSCodeCommands.Open, arguments: [this] }; 194 | } 195 | 196 | export class Spec extends GaugeNode { 197 | constructor( 198 | public readonly label: string, 199 | public readonly file: string 200 | ) { 201 | super(label, vscode.TreeItemCollapsibleState.Collapsed, file); 202 | } 203 | 204 | contextValue = 'specification'; 205 | } 206 | 207 | export class Scenario extends GaugeNode { 208 | 209 | constructor( 210 | public readonly label: string, 211 | public readonly file: string, 212 | public readonly lineNo: number, 213 | ) { 214 | super(label, vscode.TreeItemCollapsibleState.None, file); 215 | } 216 | 217 | readonly executionIdentifier = this.file + ":" + this.lineNo; 218 | 219 | contextValue = 'scenario'; 220 | } 221 | 222 | function isSpecExplorerEnabled(): boolean { 223 | let specExplorerConfig = workspace.getConfiguration('gauge.specExplorer'); 224 | return specExplorerConfig && specExplorerConfig.get('enabled'); 225 | } -------------------------------------------------------------------------------- /src/gaugeWorkspace.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { platform } from 'os'; 4 | import * as path from 'path'; 5 | import { 6 | CancellationTokenSource, commands, Disposable, OutputChannel, Uri, 7 | window, workspace, WorkspaceConfiguration, WorkspaceFoldersChangeEvent 8 | } from "vscode"; 9 | import { DynamicFeature, LanguageClient, LanguageClientOptions, RevealOutputChannelOn, ServerOptions } from "vscode-languageclient/node"; 10 | import { CLI } from './cli'; 11 | import GaugeConfig from './config/gaugeConfig'; 12 | import { GaugeJavaProjectConfig } from './config/gaugeProjectConfig'; 13 | import { GaugeCommandContext, GaugeRunners, setCommandContext } from "./constants"; 14 | import { GaugeExecutor } from "./execution/gaugeExecutor"; 15 | import { SpecNodeProvider } from "./explorer/specExplorer"; 16 | import { SpecificationProvider } from './file/specificationFileProvider'; 17 | import { GaugeClients as GaugeProjectClientMap } from './gaugeClients'; 18 | import { GaugeState } from "./gaugeState"; 19 | import { GaugeWorkspaceFeature } from "./gaugeWorkspace.proposed"; 20 | import { GaugeProject } from './project/gaugeProject'; 21 | import { MavenProject } from './project/mavenProject'; 22 | import { ProjectFactory } from './project/projectFactory'; 23 | import { getActiveGaugeDocument, hasActiveGaugeDocument } from './util'; 24 | 25 | const DEBUG_LOG_LEVEL_CONFIG = 'enableDebugLogs'; 26 | const GAUGE_LAUNCH_CONFIG = 'gauge.launch'; 27 | const GAUGE_CODELENS_CONFIG = 'gauge.codeLenses'; 28 | const REFERENCE_CONFIG = 'reference'; 29 | 30 | export class GaugeWorkspace extends Disposable { 31 | private readonly _fileProvider: SpecificationProvider; 32 | private _executor: GaugeExecutor; 33 | private _clientsMap: GaugeProjectClientMap; 34 | private _clientLanguageMap: Map = new Map(); 35 | private _outputChannel: OutputChannel = window.createOutputChannel('gauge'); 36 | private _launchConfig: WorkspaceConfiguration; 37 | private _codeLensConfig: WorkspaceConfiguration; 38 | private _disposable: Disposable; 39 | private _specNodeProvider: SpecNodeProvider; 40 | 41 | constructor(private state: GaugeState, private cli: CLI, clientsMap: GaugeProjectClientMap) { 42 | super(() => this.dispose()); 43 | 44 | this._clientsMap = clientsMap 45 | this._executor = new GaugeExecutor(this, cli); 46 | 47 | if (workspace.workspaceFolders) { 48 | workspace.workspaceFolders.forEach(async (folder) => { 49 | await this.startServerFor(folder.uri.fsPath); 50 | }); 51 | } 52 | 53 | if (hasActiveGaugeDocument(window.activeTextEditor)) 54 | this.startServerForSpecFile(window.activeTextEditor.document.uri.fsPath); 55 | 56 | setCommandContext(GaugeCommandContext.MultiProject, this._clientsMap.size > 1); 57 | 58 | workspace.onDidChangeWorkspaceFolders(async (event) => { 59 | if (event.added) await this.onFolderAddition(event); 60 | if (event.removed) this.onFolderDeletion(event); 61 | setCommandContext(GaugeCommandContext.MultiProject, this._clientsMap.size > 1); 62 | }); 63 | this._fileProvider = new SpecificationProvider(this); 64 | this._specNodeProvider = new SpecNodeProvider(this); 65 | this._disposable = Disposable.from( 66 | this._specNodeProvider, 67 | this._executor, 68 | this._fileProvider, 69 | this.onConfigurationChange(), 70 | this.onEditorChange() 71 | ); 72 | } 73 | 74 | private onEditorChange(): Disposable { 75 | return window.onDidChangeActiveTextEditor(async (editor) => { 76 | getActiveGaugeDocument(window.activeTextEditor).then(async (p) => { 77 | if (p) await this.startServerForSpecFile(p); 78 | }); 79 | }); 80 | } 81 | 82 | private async startServerForSpecFile(file: string) { 83 | let project = ProjectFactory.getGaugeRootFromFilePath(file); 84 | await this.startServerFor(project); 85 | } 86 | 87 | setReportPath(reportPath: string) { 88 | this.state.setReportPath(reportPath.trim()); 89 | } 90 | 91 | getReportPath() { 92 | return this.state.getReportPath(); 93 | } 94 | 95 | getGaugeExecutor(): GaugeExecutor { 96 | return this._executor; 97 | } 98 | 99 | getClientsMap(): GaugeProjectClientMap { 100 | return this._clientsMap; 101 | } 102 | 103 | getClientLanguageMap(): Map { 104 | return this._clientLanguageMap; 105 | } 106 | 107 | getDefaultFolder() { 108 | let projects: any = []; 109 | this._clientsMap.forEach((v, k) => projects.push(k)); 110 | return projects.sort((a: any, b: any) => a > b)[0]; 111 | } 112 | 113 | showProjectOptions(onChange: Function) { 114 | let projectItems = []; 115 | this._clientsMap.forEach((v, k) => projectItems.push({ label: path.basename(k), description: k })); 116 | let options = { canPickMany: false, placeHolder: "Choose a project" }; 117 | return window.showQuickPick(projectItems, options).then((selected: any) => { 118 | if (selected) { 119 | return onChange(selected.description); 120 | } 121 | }, (err) => { 122 | window.showErrorMessage('Unable to select project.', err); 123 | }); 124 | } 125 | 126 | private async onFolderAddition(event: WorkspaceFoldersChangeEvent) { 127 | for (let folder of event.added) { 128 | if (!this._clientsMap.has(folder.uri.fsPath)) { 129 | await this.startServerFor(folder.uri.fsPath); 130 | } 131 | } 132 | } 133 | 134 | private onFolderDeletion(event: WorkspaceFoldersChangeEvent) { 135 | for (let folder of event.removed) { 136 | if (!this._clientsMap.has(folder.uri.fsPath)) return; 137 | let client = this._clientsMap.get(folder.uri.fsPath).client; 138 | this._clientsMap.delete(folder.uri.fsPath); 139 | client.stop(); 140 | } 141 | this._specNodeProvider.changeClient(this.getDefaultFolder()); 142 | } 143 | 144 | private async startServerFor(folder: string): Promise { 145 | if (!ProjectFactory.isGaugeProject(folder)) return; 146 | let project = ProjectFactory.get(folder); 147 | if (this._clientsMap.has(project.root())) return; 148 | process.env.GAUGE_IGNORE_RUNNER_BUILD_FAILURES = "true"; 149 | let cmd = this.cli.gaugeCommand(); 150 | let serverOptions: ServerOptions = { 151 | command: cmd.command, 152 | args: cmd.argsForSpawnType(["daemon", "--lsp", "--dir", project.root()]), 153 | options: { 154 | env: { ...process.env, ...project.envs(this.cli) }, 155 | ...cmd.defaultSpawnOptions, 156 | }, 157 | }; 158 | 159 | this._launchConfig = workspace.getConfiguration(GAUGE_LAUNCH_CONFIG); 160 | if (this._launchConfig.get(DEBUG_LOG_LEVEL_CONFIG)) { 161 | serverOptions.args.push("-l"); 162 | serverOptions.args.push("debug"); 163 | } 164 | this._codeLensConfig = workspace.getConfiguration(GAUGE_CODELENS_CONFIG); 165 | if (this._codeLensConfig.has(REFERENCE_CONFIG) && !this._codeLensConfig.get(REFERENCE_CONFIG)) { 166 | serverOptions.options.env.gauge_lsp_reference_codelens = 'false'; 167 | } 168 | let clientOptions: LanguageClientOptions = { 169 | documentSelector: [{ scheme: 'file', language: 'gauge', pattern: `${project.root()}/**/*` }], 170 | diagnosticCollectionName: 'gauge', 171 | outputChannel: this._outputChannel, 172 | revealOutputChannelOn: RevealOutputChannelOn.Never, 173 | synchronize: { 174 | configurationSection: 'gauge' 175 | }, 176 | }; 177 | clientOptions.workspaceFolder = workspace.getWorkspaceFolder(Uri.file(folder)); 178 | let languageClient = new LanguageClient('gauge', 'Gauge', serverOptions, clientOptions); 179 | this._clientsMap.set(project.root(), { project: project, client: languageClient }); 180 | await this.installRunnerFor(project); 181 | this.generateJavaConfig(project); 182 | this.registerDynamicFeatures(languageClient); 183 | await languageClient.start(); 184 | this.setLanguageId(languageClient, project.root()); 185 | } 186 | 187 | private generateJavaConfig(project: GaugeProject) { 188 | if (project.isProjectLanguage(GaugeRunners.Java) && this.cli.isPluginInstalled(GaugeRunners.Java)) { 189 | if (!(project instanceof MavenProject)) { 190 | new GaugeJavaProjectConfig(project.root(), 191 | this.cli.getGaugePluginVersion(GaugeRunners.Java), new GaugeConfig(platform())).generate(); 192 | } 193 | process.env.SHOULD_BUILD_PROJECT = "false"; 194 | } 195 | } 196 | 197 | private async installRunnerFor(project: GaugeProject): Promise { 198 | const language = project.language(); 199 | if (this.cli.isPluginInstalled(language)) return; 200 | let message = `The project ${path.basename(project.root())} requires gauge ${language} to be installed. ` + 201 | "Would you like to install it?"; 202 | let action = await window.showErrorMessage(message, { modal: true }, "Yes", "No"); 203 | if (action === "Yes") { 204 | return await this.cli.installGaugeRunner(language); 205 | } 206 | return Promise.resolve(); 207 | } 208 | 209 | private registerDynamicFeatures(languageClient: LanguageClient) { 210 | let result: Array<(DynamicFeature)> = []; 211 | result.push(new GaugeWorkspaceFeature(languageClient)); 212 | languageClient.registerFeatures(result); 213 | } 214 | 215 | private setLanguageId(languageClient: LanguageClient, projectRoot: string) { 216 | languageClient.sendRequest("gauge/getRunnerLanguage", new CancellationTokenSource().token).then( 217 | (language: string) => { 218 | this._clientLanguageMap.set(projectRoot, language); 219 | } 220 | ); 221 | 222 | } 223 | 224 | private onConfigurationChange() { 225 | return workspace.onDidChangeConfiguration((params) => { 226 | let newConfig = workspace.getConfiguration(GAUGE_LAUNCH_CONFIG); 227 | if (this._launchConfig.get(DEBUG_LOG_LEVEL_CONFIG) !== newConfig.get(DEBUG_LOG_LEVEL_CONFIG)) { 228 | let msg = 'Gauge Language Server configuration changed, please restart VS Code.'; 229 | let action = 'Restart Now'; 230 | this._launchConfig = newConfig; 231 | window.showWarningMessage(msg, action).then((selection) => { 232 | if (action === selection) { 233 | commands.executeCommand('workbench.action.reloadWindow'); 234 | } 235 | }); 236 | } 237 | }); 238 | } 239 | } 240 | --------------------------------------------------------------------------------