├── vscode
├── src
│ ├── getNonce.ts
│ ├── extension.ts
│ ├── utils.ts
│ └── TestGPTProvider.ts
├── webview-ui
│ ├── src
│ │ ├── global.d.ts
│ │ ├── main.ts
│ │ ├── types.ts
│ │ ├── lib
│ │ │ ├── Dropdown.svelte
│ │ │ ├── RemoveButton.svelte
│ │ │ ├── TextArea.svelte
│ │ │ ├── utils.ts
│ │ │ └── Logo.svelte
│ │ ├── utilities
│ │ │ ├── vscode.js.map
│ │ │ ├── vscode.ts
│ │ │ └── vscode.js
│ │ ├── App.svelte
│ │ └── Advanced.svelte
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── README.md
│ ├── package.json
│ └── rollup.config.js
├── media
│ ├── logo.png
│ └── icon.svg
├── tsconfig.json
├── .prettierrc
├── .prettierignore
├── .eslintrc.json
├── .vscodeignore
├── LICENSE
├── package.json
└── resources
│ └── default.yaml
├── .gitignore
├── show.gif
├── media
├── logo.png
├── show.gif
└── icon.svg
├── src
├── const.ts
├── index.ts
├── __tests__
│ ├── const.test.ts
│ ├── types.test.ts
│ ├── index.test.ts
│ ├── command.test.ts
│ ├── autoTest.test.ts
│ └── utils.test.ts
├── types.ts
├── command.ts
└── utils.ts
├── vitest.config.ts
├── tsconfig.json
├── LICENSE
├── package.json
├── testgpt.config.yaml
└── README.md
/vscode/src/getNonce.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bin
3 | .vscode
4 | vscode/out
--------------------------------------------------------------------------------
/vscode/webview-ui/src/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/show.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayez-nazzal/TestGPT/HEAD/show.gif
--------------------------------------------------------------------------------
/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayez-nazzal/TestGPT/HEAD/media/logo.png
--------------------------------------------------------------------------------
/media/show.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayez-nazzal/TestGPT/HEAD/media/show.gif
--------------------------------------------------------------------------------
/vscode/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fayez-nazzal/TestGPT/HEAD/vscode/media/logo.png
--------------------------------------------------------------------------------
/src/const.ts:
--------------------------------------------------------------------------------
1 | export const CONFIG_FILE_NAME = "testgpt.config.yaml";
2 | export const DEFAULT_MODEL = "gpt-3.5-turbo-16k";
3 |
--------------------------------------------------------------------------------
/vscode/webview-ui/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /public/build/
3 | dist
4 | dist-ssr
5 | build
6 | build-ssr
7 |
8 | .DS_Store
9 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { parseCommand, executeCommand } from "./command";
3 |
4 | const options = parseCommand();
5 |
6 | executeCommand(options);
7 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/main.ts:
--------------------------------------------------------------------------------
1 | import App from "./App.svelte";
2 |
3 | const app = new (App as any)({
4 | target: document.body,
5 | });
6 |
7 | export default app;
8 |
--------------------------------------------------------------------------------
/vscode/webview-ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 | "include": ["src/**/*"],
4 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"],
5 | "compilerOptions": {
6 | "noImplicitAny": false
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/vscode/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "out",
6 | "lib": ["es6", "dom"],
7 | "sourceMap": true,
8 | "rootDir": "src",
9 | "strict": true
10 | },
11 | "exclude": ["node_modules", ".vscode-test", "webview-ui"]
12 | }
13 |
--------------------------------------------------------------------------------
/vscode/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 110,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": false,
7 | "quoteProps": "consistent",
8 | "jsxSingleQuote": false,
9 | "trailingComma": "es5",
10 | "bracketSpacing": true,
11 | "jsxBracketSameLine": true,
12 | "arrowParens": "always"
13 | }
14 |
--------------------------------------------------------------------------------
/vscode/.prettierignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 | /dist
12 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | environment: 'node',
6 | include: [
7 | 'src/**/*.test.ts',
8 | 'src/**/__tests__/**/*.test.ts'
9 | ],
10 | coverage: {
11 | reporter: ['text', 'html'],
12 | }
13 | }
14 | })
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/__tests__/const.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { CONFIG_FILE_NAME, DEFAULT_MODEL } from '../const'
3 |
4 | describe('const', () => {
5 | it('exposes constants', () => {
6 | expect(CONFIG_FILE_NAME).toBe('testgpt.config.yaml')
7 | expect(DEFAULT_MODEL).toBe('gpt-3.5-turbo-16k')
8 | })
9 | })
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "moduleResolution": "Node",
5 | "esModuleInterop": true,
6 | "target": "ES6",
7 | "sourceMap": true,
8 | "outDir": "bin",
9 | "strictNullChecks": true,
10 | },
11 | "include": ["src/**/*"],
12 | "exclude": ["node_modules", "bin", "src/**/*.test.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/vscode/src/extension.ts:
--------------------------------------------------------------------------------
1 | import { window, ExtensionContext } from "vscode";
2 | import { TestGPTWebviewProvider } from "./TestGPTProvider";
3 |
4 | export const activate = async (context: ExtensionContext) => {
5 | const provider = new TestGPTWebviewProvider(context.extensionUri, context);
6 |
7 | context.subscriptions.push(window.registerWebviewViewProvider(TestGPTWebviewProvider.viewType, provider));
8 | };
9 |
--------------------------------------------------------------------------------
/vscode/webview-ui/README.md:
--------------------------------------------------------------------------------
1 | # `webview-ui` Directory
2 |
3 | This directory contains all of the code that will be executed within the webview context. It can be thought of as the place where all the "frontend" code of a webview is contained.
4 |
5 | Types of content that can be contained here:
6 |
7 | - Frontend framework code (i.e. Svelte, Vue, SolidJS, React, etc.)
8 | - JavaScript files
9 | - CSS files
10 | - Assets / resources (i.e. images, illustrations, etc.)
11 |
--------------------------------------------------------------------------------
/vscode/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": ["@typescript-eslint"],
9 | "rules": {
10 | "@typescript-eslint/naming-convention": "warn",
11 | "@typescript-eslint/semi": "warn",
12 | "curly": "warn",
13 | "eqeqeq": "warn",
14 | "no-throw-literal": "warn",
15 | "semi": "off"
16 | },
17 | "ignorePatterns": ["webview-ui/**"]
18 | }
19 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface IPreset {
2 | name: string;
3 | config: {
4 | model: string;
5 | streaming: boolean;
6 | systemMessage: string;
7 | promptTemplate: string;
8 | instructions: string;
9 | autoTechs?: boolean;
10 | techs?: string[];
11 | examples?: {
12 | fileName: string;
13 | code: string;
14 | tests: string;
15 | }[];
16 | };
17 | }
18 |
19 | export interface IVscodeState {
20 | presets: IPreset[];
21 | activePreset: IPreset;
22 | advanced: boolean;
23 | }
24 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/lib/Dropdown.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | {#each options as option}
16 | {option}
17 | {/each}
18 |
19 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/lib/RemoveButton.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
17 |
18 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/lib/TextArea.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 | {label}
18 |
19 |
--------------------------------------------------------------------------------
/src/__tests__/types.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { examplesSchema, ERole } from '../types'
3 |
4 | describe('types', () => {
5 | it('examplesSchema describes an array of examples', () => {
6 | expect(examplesSchema.type).toBe('array')
7 | expect(examplesSchema.items?.type).toBe('object')
8 | expect((examplesSchema.items as any).required).toContain('fileName')
9 | })
10 |
11 | it('ERole enum has expected values', () => {
12 | expect(ERole.User).toBe('user')
13 | expect(ERole.System).toBe('system')
14 | expect(ERole.Assistant).toBe('assistant')
15 | })
16 | })
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi } from 'vitest'
2 |
3 | vi.mock('../command', () => {
4 | return {
5 | parseCommand: vi.fn(() => ({ mocked: true } as any)),
6 | executeCommand: vi.fn(() => undefined)
7 | }
8 | })
9 |
10 | describe('index entry', () => {
11 | it('imports and calls executeCommand', async () => {
12 | const mockExit = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { throw new Error(`exit:${code}`) }) as any)
13 | let failed = false
14 | try {
15 | await import('../index')
16 | } catch (e) {
17 | failed = true
18 | }
19 | expect(failed).toBe(false)
20 | mockExit.mockRestore()
21 | })
22 | })
23 |
24 |
25 |
--------------------------------------------------------------------------------
/vscode/.vscodeignore:
--------------------------------------------------------------------------------
1 | # This file contains all the files/directories that should
2 | # be ignored (i.e. not included) in the final packaged extension.
3 |
4 | # Ignore extension configs
5 | .vscode/**
6 |
7 | # Ignore test files
8 | .vscode-test/**
9 | out/test/**
10 |
11 | # Ignore source code
12 | src/**
13 |
14 | # Ignore all webview-ui files except the build directory
15 | webview-ui/src/**
16 | webview-ui/scripts/**
17 | webview-ui/index.html
18 | webview-ui/README.md
19 | webview-ui/package.json
20 | webview-ui/package-lock.json
21 | webview-ui/node_modules/**
22 |
23 | # Ignore Misc
24 | .yarnrc
25 | vsc-extension-quickstart.md
26 | **/.gitignore
27 | **/tsconfig.json
28 | **/vite.config.ts
29 | **/.eslintrc.json
30 | **/*.map
31 | **/*.ts
32 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/utilities/vscode.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"vscode.js","sourceRoot":"","sources":["vscode.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;GAQG;AACH,MAAM,gBAAgB;IAGpB;QACE,2EAA2E;QAC3E,2DAA2D;QAC3D,IAAI,OAAO,gBAAgB,KAAK,UAAU,EAAE;YAC1C,IAAI,CAAC,SAAS,GAAG,gBAAgB,EAAE,CAAC;SACrC;IACH,CAAC;IAED;;;;;;;OAOG;IACI,WAAW,CAAC,OAAgB;QACjC,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;SACrC;aAAM;YACL,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;SACtB;IACH,CAAC;IAED;;;;;;;OAOG;IACI,QAAQ;QACb,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;SAClC;aAAM;YACL,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;YAClD,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;SAC9C;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACI,QAAQ,CAAgC,QAAW;QACxD,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;SAC1C;aAAM;YACL,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC9D,OAAO,QAAQ,CAAC;SACjB;IACH,CAAC;CACF;AAED,+EAA+E;AAClE,QAAA,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC"}
--------------------------------------------------------------------------------
/vscode/webview-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testgpt-app",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -c -w",
8 | "start": "sirv public --no-clear",
9 | "check": "svelte-check --tsconfig ./tsconfig.json"
10 | },
11 | "dependencies": {
12 | "@tsconfig/svelte": "^5.0.2",
13 | "@vscode/webview-ui-toolkit": "^1.2.2",
14 | "sirv-cli": "^2.0.0"
15 | },
16 | "devDependencies": {
17 | "@rollup/plugin-commonjs": "^17.0.0",
18 | "@rollup/plugin-node-resolve": "^11.0.0",
19 | "@rollup/plugin-typescript": "^8.0.0",
20 | "@types/vscode-webview": "^1.57.0",
21 | "rollup": "^2.3.4",
22 | "rollup-plugin-css-only": "^3.1.0",
23 | "rollup-plugin-livereload": "^2.0.0",
24 | "rollup-plugin-svelte": "^7.1.6",
25 | "rollup-plugin-terser": "^7.0.0",
26 | "svelte": "^4.2.0",
27 | "svelte-check": "^3.5.0",
28 | "svelte-preprocess": "^5.0.4",
29 | "tslib": "^2.6.2",
30 | "typescript": "^5.2.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import type { IPreset } from "../types";
2 | import { vscode } from "../utilities/vscode";
3 |
4 | export const getWebviewState = () => {
5 | let presets = ((window as any).presets as IPreset[]) || [];
6 | let activePreset = ((window as any).activePreset as IPreset) || {
7 | name: "Default Preset",
8 | config: {},
9 | };
10 | let advanced = ((window as any).advanced as boolean) || false;
11 |
12 | const newState = vscode.getState();
13 |
14 | if (newState?.presets) {
15 | presets = newState.presets;
16 | }
17 |
18 | if (newState?.activePreset) {
19 | activePreset = newState.activePreset;
20 | }
21 |
22 | if (newState?.advanced) {
23 | advanced = newState.advanced;
24 | }
25 |
26 | return {
27 | presets,
28 | activePreset,
29 | advanced,
30 | };
31 | };
32 |
33 | export const setWebviewState = (key: string, value: any) => {
34 | vscode.setState({
35 | ...getWebviewState(),
36 | [key]: value,
37 | });
38 |
39 | window[key] = value;
40 | };
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Fayez Nazzal
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.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testgpt",
3 | "version": "4.1.0",
4 | "description": "",
5 | "main": "index.js",
6 | "types": "bin/index.d.ts",
7 | "bin": {
8 | "testgpt": "bin/index.js"
9 | },
10 | "scripts": {
11 | "build": "tsc",
12 | "test": "vitest run",
13 | "prepublishOnly": "npm run build"
14 | },
15 | "keywords": [
16 | "gpt",
17 | "openai",
18 | "cli",
19 | "command-line",
20 | "command-line-tool",
21 | "cmd",
22 | "unit testing",
23 | "testing",
24 | "test",
25 | "auto",
26 | "automation"
27 | ],
28 | "author": "Fayez Nazzal",
29 | "license": "MIT",
30 | "devDependencies": {
31 | "@types/node": "^18.14.0",
32 | "typescript": "^4.9.5",
33 | "vitest": "^2.0.5"
34 | },
35 | "dependencies": {
36 | "ajv": "^8.12.0",
37 | "axios": "^1.3.4",
38 | "chalk": "^4.1.0",
39 | "commander": "^10.0.0",
40 | "openai": "^4.4.0",
41 | "tree-sitter-cli": "^0.20.7",
42 | "yaml": "^2.3.1"
43 | },
44 | "repository": "https://github.com/fayez-nazzal/testgpt",
45 | "bugs": "https://github.com/fayez-nazzal/testgpt/issues"
46 | }
47 |
--------------------------------------------------------------------------------
/vscode/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Fayez Nazzal
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.
--------------------------------------------------------------------------------
/vscode/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { Uri, Webview } from "vscode";
2 |
3 | /**
4 | * A helper function which will get the webview URI of a given file or resource.
5 | *
6 | * @remarks This URI can be used within a webview's HTML as a link to the
7 | * given file/resource.
8 | *
9 | * @param webview A reference to the extension webview
10 | * @param extensionUri The URI of the directory containing the extension
11 | * @param pathList An array of strings representing the path to a file/resource
12 | * @returns A URI pointing to the file/resource
13 | */
14 | export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) {
15 | return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList));
16 | }
17 |
18 | /**
19 | * A helper function that returns a unique alphanumeric identifier called a nonce.
20 | *
21 | * @remarks This function is primarily used to help enforce content security
22 | * policies for resources/scripts being executed in a webview context.
23 | *
24 | * @returns A nonce
25 | */
26 | export function getNonce() {
27 | let text = "";
28 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
29 | for (let i = 0; i < 32; i++) {
30 | text += possible.charAt(Math.floor(Math.random() * possible.length));
31 | }
32 | return text;
33 | }
34 |
--------------------------------------------------------------------------------
/src/__tests__/command.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2 | import fs from 'fs'
3 | import path from 'path'
4 | import { parseCommand, executeCommand, executeForFile } from '../command'
5 |
6 | const mockExit = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { throw new Error(`exit:${code}`) }) as any)
7 | const mockCwd = vi.spyOn(process, 'cwd')
8 |
9 | describe('command', () => {
10 | const tmpDir = path.join(process.cwd(), '.vitest-tmp-cmd')
11 |
12 | beforeEach(() => {
13 | vi.restoreAllMocks()
14 | if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir)
15 | mockCwd.mockReturnValue(tmpDir)
16 | })
17 |
18 | afterEach(() => {
19 | if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true })
20 | })
21 |
22 | it('executeForFile errors without inputFile', async () => {
23 | await expect(async () => {
24 | await executeForFile({
25 | inputFile: '' as any,
26 | outputFile: path.join(tmpDir, 'out.test.ts'),
27 | apiKey: 'k',
28 | model: 'gpt-3.5-turbo-16k',
29 | systemMessage: '',
30 | promptTemplate: '',
31 | techs: '',
32 | instructions: '',
33 | examples: [],
34 | stream: false as any,
35 | modelEndpoint: ''
36 | } as any)
37 | }).rejects.toThrow()
38 | })
39 | })
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/__tests__/autoTest.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2 | import fs from 'fs'
3 | import path from 'path'
4 | import { autoTest } from '../utils'
5 |
6 | describe('autoTest', () => {
7 | const tmpDir = path.join(process.cwd(), '.vitest-tmp-auto')
8 |
9 | beforeEach(() => {
10 | if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir)
11 | })
12 |
13 | afterEach(() => {
14 | if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true })
15 | })
16 |
17 | it('writes response text when using modelEndpoint', async () => {
18 | const input = path.join(tmpDir, 'in.ts')
19 | const output = path.join(tmpDir, 'out.test.ts')
20 | fs.writeFileSync(input, 'export const x = 1')
21 |
22 | const mockFetch = vi.spyOn(global, 'fetch' as any).mockResolvedValue({
23 | text: async () => 'TEST_CONTENT'
24 | } as any)
25 |
26 | await autoTest({
27 | inputFile: input,
28 | outputFile: output,
29 | apiKey: 'k',
30 | model: 'gpt-3.5-turbo-16k',
31 | systemMessage: undefined,
32 | promptTemplate: undefined,
33 | examples: [],
34 | techs: [],
35 | instructions: [],
36 | stream: false,
37 | modelEndpoint: 'http://local/mock'
38 | })
39 |
40 | expect(fs.readFileSync(output, 'utf-8')).toBe('TEST_CONTENT')
41 | mockFetch.mockRestore()
42 | })
43 | })
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { JSONSchemaType } from "ajv";
2 |
3 | export interface IConfig {
4 | [key: `.${string}`]: {
5 | techs: string[];
6 | instructions: string[];
7 | examples: IExample[];
8 | };
9 | }
10 |
11 | export interface IGetPromptArgs {
12 | content: string;
13 | fileName: string;
14 | techs?: string[];
15 | instructions?: string[];
16 | promptTemplate?: string;
17 | }
18 |
19 | export interface IExample {
20 | fileName: string;
21 | code: string;
22 | tests: string;
23 | }
24 |
25 | export const examplesSchema: JSONSchemaType = {
26 | type: "array",
27 | items: {
28 | type: "object",
29 | required: ["fileName", "code", "tests"],
30 | properties: {
31 | fileName: {
32 | type: "string",
33 | },
34 | code: {
35 | type: "string",
36 | },
37 | tests: {
38 | type: "string",
39 | },
40 | },
41 | },
42 | };
43 |
44 | export enum ERole {
45 | User = "user",
46 | System = "system",
47 | Assistant = "assistant",
48 | }
49 |
50 | export interface IMessage {
51 | role: ERole;
52 | content: string;
53 | }
54 |
55 | export interface ICommandArgs {
56 | inputFile: string;
57 | outputFile?: string;
58 | apiKey: string;
59 | model: string;
60 | systemMessage: string;
61 | promptTemplate: string;
62 | techs: string;
63 | instructions: string;
64 | examples: IExample[];
65 | config?: string;
66 | stream: boolean;
67 | modelEndpoint: string;
68 | help: boolean;
69 | }
70 |
--------------------------------------------------------------------------------
/vscode/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testgpt",
3 | "displayName": "TestGPT",
4 | "description": "Automatic AI Powered Testing for your code.",
5 | "icon": "media/logo.png",
6 | "version": "0.0.10",
7 | "publisher": "FayezNazzal",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/fayez-nazzal/testgpt"
11 | },
12 | "engines": {
13 | "vscode": "^1.76.0"
14 | },
15 | "main": "./out/extension.js",
16 | "contributes": {
17 | "commands": [
18 | {
19 | "command": "testgpt.autoTest",
20 | "title": "Run TestGPT for this file"
21 | }
22 | ],
23 | "configuration": [
24 | {
25 | "title": "TestGPT",
26 | "properties": {
27 | "testgpt.apiKey": {
28 | "type": "string",
29 | "default": "",
30 | "description": "OpenAI API Key"
31 | },
32 | "testgpt.advancedMode": {
33 | "type": "boolean",
34 | "default": false,
35 | "description": "Advanced mode"
36 | }
37 | }
38 | }
39 | ],
40 | "viewsContainers": {
41 | "activitybar": [
42 | {
43 | "id": "testgpt",
44 | "title": "TestGPT",
45 | "icon": "media/icon.svg"
46 | }
47 | ]
48 | },
49 | "views": {
50 | "testgpt": [
51 | {
52 | "type": "webview",
53 | "id": "testgpt",
54 | "name": "TestGPT"
55 | }
56 | ]
57 | }
58 | },
59 | "scripts": {
60 | "install:all": "npm install && cd webview-ui && npm install",
61 | "start:webview": "cd webview-ui && npm run dev",
62 | "build:webview": "cd webview-ui && npm run build",
63 | "vscode:prepublish": "npm run compile",
64 | "compile": "npm run build:webview && tsc -p ./",
65 | "watch": "tsc -watch -p ./",
66 | "pretest": "npm run compile && npm run lint",
67 | "lint": "eslint src --ext ts"
68 | },
69 | "devDependencies": {
70 | "@types/glob": "^7.1.3",
71 | "@types/node": "^12.11.7",
72 | "@types/vscode": "^1.46.0",
73 | "@typescript-eslint/eslint-plugin": "^4.14.1",
74 | "@typescript-eslint/parser": "^4.14.1",
75 | "eslint": "^7.19.0",
76 | "glob": "^7.1.6",
77 | "prettier": "^2.2.1",
78 | "typescript": "^4.1.3",
79 | "vscode-test": "^1.5.0"
80 | },
81 | "dependencies": {
82 | "yaml": "^2.3.2"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/vscode/webview-ui/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import livereload from 'rollup-plugin-livereload';
5 | import { terser } from 'rollup-plugin-terser';
6 | import sveltePreprocess from 'svelte-preprocess';
7 | import typescript from '@rollup/plugin-typescript';
8 | import css from 'rollup-plugin-css-only';
9 |
10 | const production = !process.env.ROLLUP_WATCH;
11 |
12 | function serve() {
13 | let server;
14 |
15 | function toExit() {
16 | if (server) server.kill(0);
17 | }
18 |
19 | return {
20 | writeBundle() {
21 | if (server) return;
22 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
23 | stdio: ['ignore', 'inherit', 'inherit'],
24 | shell: true
25 | });
26 |
27 | process.on('SIGTERM', toExit);
28 | process.on('exit', toExit);
29 | }
30 | };
31 | }
32 |
33 | export default {
34 | input: 'src/main.ts',
35 | output: {
36 | sourcemap: true,
37 | format: 'iife',
38 | name: 'app',
39 | file: 'public/build/bundle.js'
40 | },
41 | plugins: [
42 | svelte({
43 | preprocess: sveltePreprocess({ sourceMap: !production }),
44 | compilerOptions: {
45 | // enable run-time checks when not in production
46 | dev: !production
47 | }
48 | }),
49 | // we'll extract any component CSS out into
50 | // a separate file - better for performance
51 | css({ output: 'bundle.css' }),
52 |
53 | // If you have external dependencies installed from
54 | // npm, you'll most likely need these plugins. In
55 | // some cases you'll need additional configuration -
56 | // consult the documentation for details:
57 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
58 | resolve({
59 | browser: true,
60 | dedupe: ['svelte']
61 | }),
62 | commonjs(),
63 | typescript({
64 | sourceMap: !production,
65 | inlineSources: !production
66 | }),
67 |
68 | // In dev mode, call `npm run start` once
69 | // the bundle has been generated
70 | !production && serve(),
71 |
72 | // Watch the `public` directory and refresh the
73 | // browser on changes when not in production
74 | !production && livereload('public'),
75 |
76 | // If we're building for production (npm run build
77 | // instead of npm run dev), minify
78 | production && terser()
79 | ],
80 | watch: {
81 | clearScreen: false
82 | }
83 | };
84 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/App.svelte:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 | {#if !advanced}
36 |
37 | Choose a preset
38 | p.name)} setValue={onPresetChange} />
39 |
40 |
41 |
42 | Generate tests for the active file
43 | Generate Tests
44 |
45 | {:else}
46 |
47 | {/if}
48 |
49 |
50 |
51 |
52 | {#if !advanced}
53 | Advanced Mode
54 | {:else}
55 | Simple Mode
56 | {/if}
57 |
58 |
59 |
60 |
61 |
62 |
63 | Enjoy the automation!
64 |
65 |
66 |
67 |
91 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/utilities/vscode.ts:
--------------------------------------------------------------------------------
1 | import type { WebviewApi } from "vscode-webview";
2 | import type { IVscodeState } from "../types";
3 |
4 | /**
5 | * A utility wrapper around the acquireVsCodeApi() function, which enables
6 | * message passing and state management between the webview and extension
7 | * contexts.
8 | *
9 | * This utility also enables webview code to be run in a web browser-based
10 | * dev server by using native web browser features that mock the functionality
11 | * enabled by acquireVsCodeApi.
12 | */
13 | class VSCodeAPIWrapper {
14 | private readonly vsCodeApi: WebviewApi | undefined;
15 |
16 | constructor() {
17 | // Check if the acquireVsCodeApi function exists in the current development
18 | // context (i.e. VS Code development window or web browser)
19 | if (typeof acquireVsCodeApi === "function") {
20 | this.vsCodeApi = acquireVsCodeApi();
21 | }
22 | }
23 |
24 | /**
25 | * Post a message (i.e. send arbitrary data) to the owner of the webview.
26 | *
27 | * @remarks When running webview code inside a web browser, postMessage will instead
28 | * log the given message to the console.
29 | *
30 | * @param message Abitrary data (must be JSON serializable) to send to the extension context.
31 | */
32 | public postMessage(message: unknown) {
33 | if (this.vsCodeApi) {
34 | this.vsCodeApi.postMessage(message);
35 | } else {
36 | console.log(message);
37 | }
38 | }
39 |
40 | /**
41 | * Get the persistent state stored for this webview.
42 | *
43 | * @remarks When running webview source code inside a web browser, getState will retrieve state
44 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
45 | *
46 | * @return The current state or `undefined` if no state has been set.
47 | */
48 | public getState(): IVscodeState {
49 | return this.vsCodeApi?.getState() as IVscodeState;
50 | }
51 |
52 | /**
53 | * Set the persistent state stored for this webview.
54 | *
55 | * @remarks When running webview source code inside a web browser, setState will set the given
56 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
57 | *
58 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved
59 | * using {@link getState}.
60 | *
61 | * @return The new state.
62 | */
63 | public setState(newState: T) {
64 | this.vsCodeApi?.setState(newState);
65 | }
66 | }
67 |
68 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi.
69 | export const vscode = new VSCodeAPIWrapper();
70 |
--------------------------------------------------------------------------------
/src/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2 | import fs from 'fs'
3 | import path from 'path'
4 | import {
5 | readFile,
6 | writeToFile,
7 | divideFileName,
8 | getFileType,
9 | EFileType,
10 | toList,
11 | parseTemplatePrompt,
12 | getPrompt,
13 | getExampleMessages,
14 | readYamlFile,
15 | getMessages,
16 | } from '../utils'
17 |
18 | describe('utils', () => {
19 | const tmpDir = path.join(process.cwd(), '.vitest-tmp')
20 |
21 | beforeEach(() => {
22 | if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir)
23 | })
24 |
25 | afterEach(() => {
26 | if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true })
27 | })
28 |
29 | it('readFile returns file content', () => {
30 | const file = path.join(tmpDir, 'a.txt')
31 | fs.writeFileSync(file, 'hello')
32 | expect(readFile(file)).toBe('hello')
33 | })
34 |
35 | it('writeToFile writes content', () => {
36 | const file = path.join(tmpDir, 'b.txt')
37 | writeToFile(file, 'world')
38 | expect(fs.readFileSync(file, 'utf-8')).toBe('world')
39 | })
40 |
41 | it('divideFileName returns name and extension', () => {
42 | expect(divideFileName('/x/y/file.ts')).toEqual({ name: 'file', extension: '.ts' })
43 | })
44 |
45 | it('getFileType returns file or directory', () => {
46 | const dir = path.join(tmpDir, 'dir')
47 | const file = path.join(tmpDir, 'file.txt')
48 | fs.mkdirSync(dir)
49 | fs.writeFileSync(file, '')
50 | expect(getFileType(dir)).toBe(EFileType.Directory)
51 | expect(getFileType(file)).toBe(EFileType.File)
52 | })
53 |
54 | it('toList formats list', () => {
55 | expect(toList(['a', 'b'])).toBe('1. a\r\n2. b')
56 | })
57 |
58 | it('parseTemplatePrompt replaces placeholders', () => {
59 | expect(parseTemplatePrompt('a {x} b', { x: 1 })).toBe('a 1 b')
60 | })
61 |
62 | it('getPrompt builds prompt with defaults', () => {
63 | const prompt = getPrompt({ content: 'CODE', fileName: 'f.ts' })
64 | expect(prompt).toContain('f.ts')
65 | expect(prompt).toContain('CODE')
66 | })
67 |
68 | it('getExampleMessages turns examples into messages', () => {
69 | const messages = getExampleMessages(
70 | { content: 'c', fileName: 'f.ts' },
71 | [{ fileName: 'e.ts', code: 'x', tests: 'y' }]
72 | )
73 | expect(messages.length).toBe(2)
74 | expect(messages[0].role).toBe('user')
75 | expect(messages[1].role).toBe('assistant')
76 | })
77 |
78 | it('readYamlFile parses yaml', () => {
79 | const file = path.join(tmpDir, 'c.yaml')
80 | fs.writeFileSync(file, 'a: 1')
81 | expect(readYamlFile(file)).toEqual({ a: 1 })
82 | })
83 |
84 | it('getMessages builds message array', () => {
85 | const messages = getMessages(undefined, 'hi', [])
86 | expect(messages[0].role).toBe('system')
87 | expect(messages[messages.length - 1].content).toBe('hi')
88 | })
89 | })
90 |
91 |
92 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/utilities/vscode.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.vscode = void 0;
4 | /**
5 | * A utility wrapper around the acquireVsCodeApi() function, which enables
6 | * message passing and state management between the webview and extension
7 | * contexts.
8 | *
9 | * This utility also enables webview code to be run in a web browser-based
10 | * dev server by using native web browser features that mock the functionality
11 | * enabled by acquireVsCodeApi.
12 | */
13 | class VSCodeAPIWrapper {
14 | constructor() {
15 | // Check if the acquireVsCodeApi function exists in the current development
16 | // context (i.e. VS Code development window or web browser)
17 | if (typeof acquireVsCodeApi === "function") {
18 | this.vsCodeApi = acquireVsCodeApi();
19 | }
20 | }
21 | /**
22 | * Post a message (i.e. send arbitrary data) to the owner of the webview.
23 | *
24 | * @remarks When running webview code inside a web browser, postMessage will instead
25 | * log the given message to the console.
26 | *
27 | * @param message Abitrary data (must be JSON serializable) to send to the extension context.
28 | */
29 | postMessage(message) {
30 | if (this.vsCodeApi) {
31 | this.vsCodeApi.postMessage(message);
32 | }
33 | else {
34 | console.log(message);
35 | }
36 | }
37 | /**
38 | * Get the persistent state stored for this webview.
39 | *
40 | * @remarks When running webview source code inside a web browser, getState will retrieve state
41 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
42 | *
43 | * @return The current state or `undefined` if no state has been set.
44 | */
45 | getState() {
46 | if (this.vsCodeApi) {
47 | return this.vsCodeApi.getState();
48 | }
49 | else {
50 | const state = localStorage.getItem("vscodeState");
51 | return state ? JSON.parse(state) : undefined;
52 | }
53 | }
54 | /**
55 | * Set the persistent state stored for this webview.
56 | *
57 | * @remarks When running webview source code inside a web browser, setState will set the given
58 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
59 | *
60 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved
61 | * using {@link getState}.
62 | *
63 | * @return The new state.
64 | */
65 | setState(newState) {
66 | if (this.vsCodeApi) {
67 | return this.vsCodeApi.setState(newState);
68 | }
69 | else {
70 | localStorage.setItem("vscodeState", JSON.stringify(newState));
71 | return newState;
72 | }
73 | }
74 | }
75 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi.
76 | exports.vscode = new VSCodeAPIWrapper();
77 | //# sourceMappingURL=vscode.js.map
--------------------------------------------------------------------------------
/testgpt.config.yaml:
--------------------------------------------------------------------------------
1 | .ts:
2 | techs:
3 | - jest
4 | instructions:
5 | - use 2 spaces for indentation
6 | - wrap each group of tests in a describe block
7 | - Don't forget to import the things you need.
8 | examples:
9 | - fileName: array_utils.test.ts
10 | code: |
11 | export const addItem = (array: any[], item: any) => {
12 | return [...array, item];
13 | };
14 |
15 | export const removeItem = (array: any[], item: any) => {
16 | return array.filter((i) => i !== item);
17 | };
18 |
19 | export const toggleItem = (array: any[], item: any) => {
20 | return array.includes(item) ? removeItem(array, item) : addItem(array, item);
21 | };
22 |
23 | export const toggleItemInArray = (array: any[], item: any) => {
24 | return array.includes(item) ? removeItem(array, item) : addItem(array, item);
25 | };
26 | tests: |
27 | import {
28 | addItem,
29 | removeItem,
30 | toggleItem,
31 | toggleItemInArray,
32 | } from "./array_utils";
33 |
34 | describe("array_utils", () => {
35 | it("should add item to the array", () => {
36 | const array = ["item1", "item2"];
37 | const itemToAdd = "item3";
38 |
39 | expect(addItem(array, itemToAdd)).toEqual([...array, itemToAdd]);
40 | });
41 |
42 | describe("removeItem", () => {
43 | it("should remove item from the array", () => {
44 | const array = ["item1", "item2", "item3"];
45 | const itemToRemove = "item2";
46 |
47 | expect(removeItem(array, itemToRemove)).toEqual(["item1", "item3"]);
48 | });
49 | });
50 |
51 | describe("toggleItem", () => {
52 | it("should remove the item if it already exists in the array", () => {
53 | const array = ["item1", "item2"];
54 | const itemToToggle = "item2";
55 |
56 | expect(toggleItem(array, itemToToggle)).toEqual(["item1"]);
57 | });
58 |
59 | it("should add the item if it doesn't exist in the array", () => {
60 | const array = ["item1", "item2"];
61 | const itemToToggle = "item3";
62 |
63 | expect(toggleItem(array, itemToToggle)).toEqual([...array, itemToToggle]);
64 | });
65 | });
66 |
67 | describe("toggleItemInArray", () => {
68 | it("should remove the item if it already exists in the array", () => {
69 | const array = ["item1", "item2"];
70 | const itemToToggle = "item2";
71 |
72 | expect(toggleItemInArray(array, itemToToggle)).toEqual(["item1"]);
73 | });
74 |
75 | it("should add the item if it doesn't exist in the array", () => {
76 | const array = ["item1", "item2"];
77 | const itemToToggle = "item3";
78 |
79 | expect(toggleItemInArray(array, itemToToggle)).toEqual([
80 | ...array,
81 | itemToToggle,
82 | ]);
83 | });
84 | });
85 | });
86 | - fileName: data.ts
87 | code: |
88 | import axios from "axios";
89 | import chalk from "chalk";
90 |
91 | export const fetchDogNames = async () => {
92 | console.log(chalk.blue("Fetching dog names..."));
93 | const response = await fetch("https://dog.ceo/api/breeds/list/all");
94 | const data = await response.json();
95 | return Object.keys(data.message);
96 | };
97 |
98 | export const fetchCatNames = async () => {
99 | console.log(chalk.blue("Fetching cat names..."));
100 | const response = await axios.get("https://api.thecatapi.com/v1/breeds");
101 | return response.data.map((cat: any) => cat.name);
102 | };
103 |
104 | export const fetchDogImage = async (breed: string) => {
105 | console.log(chalk.green(`Fetching ${breed} image...`));
106 | const response = await fetch(
107 | `https://dog.ceo/api/breed/${breed}/images/random`
108 | );
109 | const data = await response.json();
110 | return data.message;
111 | };
112 | tests: |
113 | import axios from "axios";
114 | import { fetchDogImage, fetchCatNames, fetchDogNames } from "./data";
115 |
116 | jest.mock("chalk", () => ({
117 | blue: jest.fn((text) => text),
118 | green: jest.fn((text) => text),
119 | }));
120 |
121 | (global as any).fetch = jest.fn();
122 |
123 | jest.mock("axios", () => ({
124 | get: jest.fn(),
125 | }));
126 |
127 | describe("data", () => {
128 | it("should fetch dog names", async () => {
129 | (global as any).fetch.mockImplementationOnce(() =>
130 | Promise.resolve({
131 | json: () => Promise.resolve({ message: { bulldog: [], poodle: [] } }),
132 | })
133 | );
134 |
135 | const dogNames = await fetchDogNames();
136 | expect(dogNames).toEqual(["bulldog", "poodle"]);
137 | });
138 |
139 | it("should fetch cat names", async () => {
140 | (axios.get as jest.Mock).mockImplementationOnce(() =>
141 | Promise.resolve({
142 | data: [{ name: "catie" }, { name: "kitty" }],
143 | })
144 | );
145 |
146 | const catNames = await fetchCatNames();
147 | expect(catNames).toEqual(["catie", "kitty"]);
148 | });
149 |
150 | it("should fetch dog image", async () => {
151 | const link = "https://dog.ceo/api/breed/bulldog/images/random";
152 |
153 | (global as any).fetch.mockImplementationOnce(() =>
154 | Promise.resolve({
155 | json: () =>
156 | Promise.resolve({
157 | message: link,
158 | }),
159 | })
160 | );
161 |
162 | const dogImage = await fetchDogImage("bulldog");
163 | expect(dogImage).toEqual(link);
164 | });
165 | });
166 |
--------------------------------------------------------------------------------
/src/command.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 |
4 | import { program } from "commander";
5 | import chalk from "chalk";
6 |
7 | import { CONFIG_FILE_NAME, DEFAULT_MODEL } from "./const";
8 | import { ICommandArgs, IConfig, examplesSchema } from "./types";
9 | import {
10 | autoTest,
11 | divideFileName,
12 | EFileType,
13 | getFileType,
14 | IModel,
15 | readYamlFile,
16 | } from "./utils";
17 | import Ajv from "ajv";
18 |
19 | export const parseCommand = () => {
20 | program
21 | .option("-i, --inputFile ")
22 | .option("-o, --outputFile ")
23 | .option("-k, --apiKey ")
24 | .option("-m, --model ")
25 | .option("-p, --promptTemplate ")
26 | .option("-y, --systemMessage ")
27 | .option("-t, --techs ")
28 | .option("-n, --instructions ")
29 | .option("-x, --examples ")
30 | .option("-c, --config ")
31 | .option("-s, --stream")
32 | .option("-e, --modelEndpoint ")
33 | .option("-h, --help");
34 |
35 | program.parse();
36 |
37 | const options = program.opts();
38 |
39 | if (options.help) {
40 | console.log(
41 | chalk.blue(
42 | `Usage: testgpt -i -o -k -m -t -p -c `
43 | )
44 | );
45 |
46 | console.log(
47 | chalk.green(
48 | "\r\nAll fields are optional except for the input file. If no output file is provided, the default will be used."
49 | )
50 | );
51 |
52 | // exit the program
53 | process.exit(0);
54 | }
55 |
56 | return options as ICommandArgs;
57 | };
58 |
59 | export const executeCommand = async (args: ICommandArgs) => {
60 | const { help, inputFile, outputFile } = args;
61 | if (help) {
62 | console.log(
63 | chalk.blue(
64 | `Usage: testgpt -i -o -k -m -t -p -c `
65 | )
66 | );
67 |
68 | console.log(
69 | chalk.green(
70 | "\r\nAll fields are optional except inputFile. If no inputFile is provided, the default will be used."
71 | )
72 | );
73 |
74 | // exit the program
75 | process.exit(0);
76 | }
77 |
78 | const isInputDirectory = getFileType(inputFile) === EFileType.Directory;
79 | const isOutputDirectory =
80 | outputFile && getFileType(outputFile) === EFileType.Directory;
81 |
82 | if (isInputDirectory && outputFile && !isOutputDirectory) {
83 | console.error(
84 | chalk.red(
85 | "If inputFile is a directory, outputFile must also be a directory"
86 | )
87 | );
88 | process.exit(1);
89 | }
90 |
91 | if (isInputDirectory) {
92 | // if outputDirectory is not provided, use the inputDirectory
93 | const outputDirectory = outputFile || inputFile;
94 |
95 | const files = fs.readdirSync(inputFile);
96 |
97 | for (const file of files) {
98 | const inputFilePath = path.join(inputFile, file);
99 | const { name: inputFileName, extension } = divideFileName(inputFilePath);
100 | const outputFilePath = path.join(
101 | outputDirectory,
102 | `${inputFileName}.test${extension}`
103 | );
104 |
105 | await executeForFile({
106 | ...args,
107 | inputFile: inputFilePath,
108 | outputFile: outputFilePath,
109 | });
110 | }
111 | } else {
112 | await executeForFile(args);
113 | }
114 |
115 | process.exit(0);
116 | };
117 |
118 | export const executeForFile = async ({
119 | inputFile,
120 | outputFile,
121 | apiKey,
122 | model,
123 | systemMessage,
124 | promptTemplate,
125 | techs,
126 | instructions,
127 | examples,
128 | config,
129 | stream,
130 | modelEndpoint,
131 | }: Omit) => {
132 | if (examples && typeof examples === "string") {
133 | const ajv = new Ajv();
134 | const validate = ajv.compile(examplesSchema);
135 |
136 | examples = JSON.parse(examples);
137 |
138 | const valid = validate(examples);
139 | if (!valid) {
140 | console.error(chalk.red("Invalid examples format"));
141 | console.error(chalk.red(JSON.stringify(validate.errors, null, 2)));
142 | process.exit(1);
143 | }
144 | }
145 |
146 | let { extension: inputFileExtension } = divideFileName(inputFile);
147 |
148 | if (!inputFile) {
149 | console.error(chalk.red("Please provide an input file"));
150 | process.exit(1);
151 | }
152 |
153 | console.log(chalk.blue(`Reading ${CONFIG_FILE_NAME}...`));
154 |
155 | const configFilePath = path.join(process.cwd(), `${CONFIG_FILE_NAME}`);
156 |
157 | let testGPTConfig: IConfig = {};
158 |
159 | if (fs.existsSync(configFilePath)) {
160 | console.log(chalk.green("Config file found, using.."));
161 |
162 | testGPTConfig = readYamlFile(
163 | config || path.join(process.cwd(), `${CONFIG_FILE_NAME}`)
164 | );
165 | } else {
166 | if (techs) {
167 | console.log(chalk.blue(`Config file not found, using passed config`));
168 |
169 | testGPTConfig = {
170 | [inputFileExtension]: {
171 | techs: techs?.split(",") || [],
172 | instructions: instructions?.split(",") || [],
173 | examples: [],
174 | },
175 | };
176 | } else {
177 | console.log(
178 | chalk.blue(`Config file not found, continuing with default config`)
179 | );
180 | }
181 | }
182 |
183 | if (!outputFile) {
184 | const inputFileWithoutExtension = inputFile.replace(inputFileExtension, "");
185 |
186 | outputFile = `${inputFileWithoutExtension}.test${inputFileExtension}`;
187 |
188 | console.log(chalk.blue("No output file provided, using default."));
189 |
190 | console.log(chalk.yellow(`Output file: ${outputFile}`));
191 | }
192 |
193 | const parsedTechs = testGPTConfig?.[inputFileExtension]?.techs;
194 | const parsedInstructions = testGPTConfig?.[inputFileExtension]?.instructions;
195 | examples ??= testGPTConfig?.[inputFileExtension]?.examples;
196 | apiKey ??= process.env.OPENAI_API_KEY as string;
197 | model ??= DEFAULT_MODEL;
198 |
199 | await autoTest({
200 | inputFile,
201 | outputFile,
202 | apiKey,
203 | model: model as IModel,
204 | systemMessage,
205 | promptTemplate,
206 | examples,
207 | techs: parsedTechs,
208 | instructions: parsedInstructions,
209 | stream,
210 | modelEndpoint,
211 | });
212 | };
213 |
--------------------------------------------------------------------------------
/vscode/resources/default.yaml:
--------------------------------------------------------------------------------
1 | - name: Auto Preset
2 | config:
3 | model: gpt-3.5-turbo-16k
4 | streaming: true
5 | systemMessage: You are my unit testing assistant, you will help write unit tests
6 | for the files I provide, your reply will only include the code, nothing
7 | more.
8 | promptTemplate: |-
9 | Please provide unit tests for the file {fileName} using {techs}
10 | {instructions}
11 |
12 | Here is the file content:
13 | ```{content}```
14 | instructions: Only respond with the code directly, don't include any additional
15 | information. don't use markdown or any other formatting.
16 | techs:
17 | autoTechs: true
18 | examples:
19 | - fileName: greetings.js
20 | code: >-
21 | function sayHello() {
22 | return 'Hello';
23 | }
24 |
25 | function sayOhayo() {
26 | return 'Ohayo';
27 | }
28 |
29 | function sayGoodbye() {
30 | return 'Goodbye';
31 | }
32 |
33 | function saySomething(thing) {
34 | return thing;
35 | }
36 |
37 | module.exports = {
38 | sayHello,
39 | sayOhayo,
40 | sayGoodbye,
41 | saySomething,
42 | };
43 | tests: >-
44 | const { sayHello, sayOhayo, sayGoodbye, saySomething } = require('./greetings');
45 |
46 | test('should say Hello', () => {
47 | expect(sayHello()).toBe('Hello');
48 | });
49 |
50 | test('should say Ohayo', () => {
51 | expect(sayOhayo()).toBe('Ohayo');
52 | });
53 |
54 | test('should say Goodbye', () => {
55 | expect(sayGoodbye()).toBe('Goodbye');
56 | });
57 |
58 | test('should say something', () => {
59 | expect(saySomething('Apple')).toBe('Apple');
60 | });
61 | - fileName: greetings.py
62 | code: |-
63 | def say_hello():
64 | return 'Hello'
65 |
66 | def say_ohayo():
67 | return 'Ohayo'
68 |
69 | def say_goodbye():
70 | return 'Goodbye'
71 |
72 | def say_something(thing):
73 | return thing
74 | tests: |-
75 | from greetings import say_hello, say_ohayo, say_goodbye, say_something
76 |
77 | def test_say_hello():
78 | assert say_hello() == 'Hello'
79 |
80 | def test_say_ohayo():
81 | assert say_ohayo() == 'Ohayo'
82 |
83 | def test_say_goodbye():
84 | assert say_goodbye() == 'Goodbye'
85 |
86 | def test_say_something():
87 | assert say_something('Apple') == 'Apple'
88 | - name: React & Jest
89 | config:
90 | model: gpt-3.5-turbo-16k
91 | streaming: true
92 | systemMessage: You are my unit testing assistant, you will help write unit tests
93 | for the files I provide, your reply will only include the code, nothing
94 | more.
95 | promptTemplate: |-
96 | Please provide unit tests for the file {fileName} using {techs}
97 | {instructions}
98 |
99 | Here is the file content:
100 | ```{content}```
101 | instructions: Follow best practices. Follow my guidelines. Only respond with the code.
102 | techs: []
103 | examples:
104 | - fileName: Header.tsx
105 | code: |-
106 | import React from 'react';
107 |
108 | interface HeaderProps {
109 | title: string;
110 | subtitle: string;
111 | }
112 |
113 | const Header: React.FC = ({ title, subtitle }) => {
114 | return (
115 |
116 |
{title}
117 | {subtitle}
118 |
119 | );
120 | };
121 |
122 | export default Header;
123 | tests: |-
124 | import React from 'react';
125 | import { render } from '@testing-library/react';
126 | import Header from './Header';
127 |
128 | test('renders title and subtitle', () => {
129 | const { getByText } = render();
130 |
131 | const titleElement = getByText(/My Title/i);
132 | const subtitleElement = getByText(/My Subtitle/i);
133 |
134 | expect(titleElement).toBeInTheDocument();
135 | expect(subtitleElement).toBeInTheDocument();
136 | });
137 | - fileName: Dropdown.tsx
138 | code: >-
139 | import React, { useState } from 'react';
140 |
141 | interface DropdownProps {
142 | options: string[];
143 | defaultOption: string;
144 | }
145 |
146 | const Dropdown: React.FC = ({ options, defaultOption }) => {
147 | const [selectedOption, setSelectedOption] = useState(defaultOption);
148 |
149 | const handleChange = (e: React.ChangeEvent) => {
150 | setSelectedOption(e.target.value);
151 | };
152 |
153 | return (
154 |
161 | );
162 | };
163 |
164 | export default Dropdown;
165 | tests: >-
166 | import React from 'react';
167 | import { render, fireEvent } from '@testing-library/react';
168 | import Dropdown from './Dropdown';
169 |
170 | test('renders options and handles selection', () => {
171 | const options = ['Apple', 'Banana', 'Cherry'];
172 | const defaultOption = 'Apple';
173 |
174 | const { getByDisplayValue } = render(
175 |
176 | );
177 |
178 | // Check initial default value
179 | expect(getByDisplayValue(defaultOption)).toBeInTheDocument();
180 |
181 | // Change selection and verify
182 | const dropdown = getByDisplayValue(defaultOption) as HTMLSelectElement;
183 | fireEvent.change(dropdown, { target: { value: 'Banana' } });
184 |
185 | expect(getByDisplayValue('Banana')).toBeInTheDocument();
186 | });
187 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import chalk from "chalk";
2 | import fs from "fs";
3 | import path from "path";
4 | import OpenAI from "openai";
5 | import { parse } from "yaml";
6 | import { ERole, IExample, IMessage, IGetPromptArgs } from "./types";
7 |
8 | export const readFile = (path: string) => {
9 | try {
10 | const fileContent: string = fs.readFileSync(path, "utf-8");
11 | return fileContent;
12 | } catch (err) {
13 | console.error(`Error reading file: ${err}`);
14 | return "";
15 | }
16 | };
17 |
18 | export const writeToFile = (
19 | path: string,
20 | content: string,
21 | append?: boolean
22 | ) => {
23 | try {
24 | fs.writeFileSync(path, content, {
25 | flag: append ? "a" : "w",
26 | });
27 | console.log(chalk.green(`Successfully wrote to file: ${path}`));
28 | } catch (err) {
29 | console.error(`Error writing to file: ${err}`);
30 | }
31 | };
32 |
33 | export const divideFileName = (fileName: string) => {
34 | const extension = path.extname(fileName);
35 | const name = path.basename(fileName, extension);
36 |
37 | return { name, extension };
38 | };
39 |
40 | export enum EFileType {
41 | File,
42 | Directory,
43 | }
44 |
45 | export const getFileType = (path: string) => {
46 | try {
47 | const isDirectory = fs.lstatSync(path).isDirectory();
48 |
49 | return isDirectory ? EFileType.Directory : EFileType.File;
50 | } catch (err) {
51 | console.error(`Error getting file type: ${err}`);
52 | return EFileType.File;
53 | }
54 | };
55 |
56 | export const toList = (arr: string[]) =>
57 | arr.map((tip, index) => `${index + 1}. ${tip}`).join("\r\n");
58 |
59 | export const parseTemplatePrompt = (template: string, args: any) => {
60 | const regex = /{(\w+)}/g;
61 |
62 | return template.replace(regex, (_, key) => {
63 | return args[key];
64 | });
65 | };
66 |
67 | export const getPrompt = ({
68 | content,
69 | fileName,
70 | techs,
71 | instructions,
72 | promptTemplate,
73 | }: IGetPromptArgs) => {
74 | let prompt =
75 | promptTemplate ??
76 | `Please provide unit tests for the file {fileName} using {techs}
77 | {instructions}
78 |
79 | Please begin your response with \`\`\` and end it with \`\`\` directly.
80 |
81 | Here is the file content:
82 | \`\`\`{content}\`\`\``;
83 |
84 | const techsCotent = techs?.length ? toList(techs) : "same techs as the file";
85 | const instructionsContent = instructions?.join("\r\n") || "";
86 |
87 | const resultPrompt = parseTemplatePrompt(prompt, {
88 | content,
89 | fileName,
90 | techs: techsCotent,
91 | instructions: instructionsContent,
92 | });
93 |
94 | return resultPrompt;
95 | };
96 |
97 | export const getExampleMessages = (
98 | promptArgs: IGetPromptArgs,
99 | examples?: IExample[]
100 | ) => {
101 | if (!examples) {
102 | return [];
103 | }
104 |
105 | const messages = examples
106 | .map((g) => {
107 | const prompt = getPrompt({
108 | ...promptArgs,
109 | content: g.code,
110 | fileName: g.fileName,
111 | });
112 |
113 | return [
114 | {
115 | role: ERole.User,
116 | content: prompt,
117 | },
118 | {
119 | role: ERole.Assistant,
120 | content: g.tests,
121 | },
122 | ];
123 | })
124 | .flat();
125 |
126 | return messages as IMessage[];
127 | };
128 |
129 | export const readYamlFile = (path: string) => {
130 | const content = readFile(path);
131 | return parse(content);
132 | };
133 |
134 | export type IModel = "gpt-3.5-turbo" | "gpt-3.5-turbo-16k" | "gpt-4";
135 |
136 | export const initOpenAI = async (apiKey: string) => {
137 | const openai = new OpenAI({
138 | apiKey: apiKey,
139 | });
140 |
141 | return openai;
142 | };
143 |
144 | export const getMessages = (
145 | systemMessage: string | undefined,
146 | prompt: string,
147 | examples: IMessage[]
148 | ) => {
149 | systemMessage ??=
150 | "You are my unit testing assistant, you will help me write unit tests for the files I provide, your reply will only include the unit tests without any additional information, starting your response with ``` and ending it with ``` directly will help me understand your response better.";
151 |
152 | return [
153 | {
154 | role: ERole.System,
155 | content: systemMessage,
156 | },
157 | ...examples,
158 | {
159 | role: ERole.User,
160 | content: prompt,
161 | },
162 | ];
163 | };
164 |
165 | interface IGetTestContentArgs {
166 | model: IModel;
167 | messages: IMessage[];
168 | openai: OpenAI;
169 | }
170 |
171 | export const getTestContent = async ({
172 | model,
173 | messages,
174 | openai,
175 | }: IGetTestContentArgs) => {
176 | const response = await openai.chat.completions.create({
177 | model,
178 | messages,
179 | });
180 |
181 | // remove lines that start with ``` (markdown code block)
182 | const regex = /^```.*$/gm;
183 | return response.choices[0].message?.content?.replace(regex, "");
184 | };
185 |
186 | export interface IStreamTestContentArgs {
187 | model: IModel;
188 | messages: IMessage[];
189 | openai: OpenAI;
190 | onToken: (token: string) => void;
191 | }
192 |
193 | export const streamTestContent = async ({
194 | model,
195 | messages,
196 | openai,
197 | onToken,
198 | }: IStreamTestContentArgs) => {
199 | const response = await openai.chat.completions.create({
200 | model,
201 | messages,
202 | stream: true,
203 | });
204 |
205 | for await (const part of response) {
206 | const content = part.choices[0].delta.content;
207 |
208 | if (content) {
209 | onToken(content);
210 | }
211 | }
212 | };
213 |
214 | interface IAutoTestArgs {
215 | inputFile: string;
216 | outputFile: string;
217 | apiKey: string;
218 | model: IModel;
219 | systemMessage?: string;
220 | promptTemplate?: string;
221 | modelEndpoint?: string;
222 | examples?: IExample[];
223 | techs?: string[];
224 | instructions?: string[];
225 | stream?: boolean;
226 | }
227 |
228 | export const autoTest = async ({
229 | inputFile,
230 | outputFile,
231 | apiKey,
232 | model,
233 | systemMessage,
234 | promptTemplate,
235 | examples,
236 | techs,
237 | instructions,
238 | stream,
239 | modelEndpoint,
240 | }: IAutoTestArgs) => {
241 | console.log(chalk.blue("Reading input file..."));
242 |
243 | let content: string;
244 | try {
245 | content = readFile(inputFile);
246 | } catch (err) {
247 | console.error(chalk.red(`Error reading file: ${err}`));
248 | process.exit(1);
249 | }
250 |
251 | console.log(chalk.blue("Generating tests..."));
252 |
253 | const promptArgs = {
254 | content,
255 | fileName: inputFile,
256 | techs,
257 | instructions,
258 | promptTemplate,
259 | };
260 |
261 | const prompt = getPrompt(promptArgs);
262 | const exampleMessages = getExampleMessages(promptArgs, examples);
263 |
264 | if (modelEndpoint) {
265 | console.log("Found model endpoint, using it instead of OpenAI API");
266 | const response = await fetch(modelEndpoint, {
267 | method: "POST",
268 | body: JSON.stringify({
269 | prompt,
270 | examples: exampleMessages,
271 | }),
272 | headers: {
273 | "Content-Type": "application/json",
274 | },
275 | });
276 |
277 | const text = await response.text();
278 | writeToFile(outputFile, text);
279 | return;
280 | }
281 |
282 | const messages = getMessages(systemMessage, prompt, exampleMessages);
283 |
284 | const openai = await initOpenAI(apiKey);
285 |
286 | if (stream) {
287 | const onToken = (token) => {
288 | writeToFile(outputFile, token, true);
289 | };
290 |
291 | await streamTestContent({
292 | openai,
293 | model,
294 | messages,
295 | onToken,
296 | });
297 | } else {
298 | const testContent = await getTestContent({
299 | openai,
300 | model,
301 | messages,
302 | });
303 |
304 | if (!testContent) {
305 | console.error(chalk.red("Error generating tests - No tests content"));
306 | process.exit(1);
307 | }
308 |
309 | writeToFile(outputFile, testContent);
310 | }
311 | };
312 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/Advanced.svelte:
--------------------------------------------------------------------------------
1 |
148 |
149 |
150 | p.name)} value={activePreset.name} setValue={onPresetChange} />
151 |
152 | Add New
153 | Delete
154 |
155 |
156 |
157 |
158 |
260 |
261 |
262 |
288 |
--------------------------------------------------------------------------------
/vscode/src/TestGPTProvider.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable curly */
2 | import {
3 | Disposable,
4 | Webview,
5 | window,
6 | Uri,
7 | WebviewViewProvider,
8 | CancellationToken,
9 | WebviewView,
10 | WebviewViewResolveContext,
11 | workspace,
12 | ConfigurationTarget,
13 | ExtensionContext,
14 | } from "vscode";
15 | import { getUri } from "./utils";
16 | import { parse, stringify } from "yaml";
17 | import * as fs from "fs";
18 | import { spawn } from "child_process";
19 |
20 | export class TestGPTWebviewProvider implements WebviewViewProvider {
21 | public static currentPanel: TestGPTWebviewProvider | undefined;
22 | public static readonly viewType = "testgpt";
23 |
24 | private _view?: WebviewView;
25 | private _disposables: Disposable[] = [];
26 | private _extensionUri: Uri;
27 | private context: ExtensionContext;
28 |
29 | /**
30 | * @param panel A reference to the webview panel
31 | * @param extensionUri The URI of the directory containing the extension
32 | */
33 | constructor(extensionUri: Uri, context: ExtensionContext) {
34 | this._extensionUri = extensionUri;
35 | this.context = context;
36 | }
37 |
38 | resolveWebviewView(
39 | webviewView: WebviewView,
40 | context: WebviewViewResolveContext,
41 | token: CancellationToken
42 | ): void | Thenable {
43 | this._view = webviewView;
44 |
45 | webviewView.webview.options = {
46 | enableScripts: true,
47 | localResourceRoots: [
48 | Uri.joinPath(this._extensionUri, "out"),
49 | Uri.joinPath(this._extensionUri, "webview-ui/public/build"),
50 | ],
51 | };
52 |
53 | webviewView.webview.html = this._getWebviewContent(webviewView.webview);
54 |
55 | this._setWebviewMessageListener(webviewView.webview);
56 | }
57 |
58 | /**
59 | * Cleans up and disposes of webview resources when the webview panel is closed.
60 | */
61 | public dispose() {
62 | TestGPTWebviewProvider.currentPanel = undefined;
63 |
64 | // Dispose of all disposables (i.e. commands) for the current webview panel
65 | while (this._disposables?.length) {
66 | const disposable = this._disposables.pop();
67 | if (disposable) {
68 | disposable.dispose();
69 | }
70 | }
71 | }
72 |
73 | /**
74 | * Defines and returns the HTML that should be rendered within the webview panel.
75 | *
76 | * @remarks This is also the place where references to the React webview build files
77 | * are created and inserted into the webview HTML.
78 | *
79 | * @param webview A reference to the extension webview
80 | * @param extensionUri The URI of the directory containing the extension
81 | * @returns A template string literal containing the HTML that should be
82 | * rendered within the webview panel
83 | */
84 | private _getWebviewContent(webview: Webview) {
85 | try {
86 | // The CSS file from the React build output
87 | const stylesUri = getUri(webview, this._extensionUri, ["webview-ui", "public", "build", "bundle.css"]);
88 |
89 | // The JS file from the React build output
90 | const scriptUri = getUri(webview, this._extensionUri, ["webview-ui", "public", "build", "bundle.js"]);
91 |
92 | // Read yaml file from resources folder
93 | const presetsUri = getUri(webview, this._extensionUri, ["resources", "default.yaml"]);
94 |
95 | const globalStorageUri = this.context.globalStorageUri;
96 | // move default.yaml to global storage if it doesn't exist
97 | if (!fs.existsSync(globalStorageUri.fsPath)) {
98 | fs.mkdirSync(globalStorageUri.fsPath);
99 | }
100 |
101 | const presetsGlobalUri = getUri(webview, globalStorageUri, ["presets.yaml"]);
102 | if (!fs.existsSync(presetsGlobalUri.fsPath)) {
103 | fs.copyFileSync(presetsUri.fsPath, presetsGlobalUri.fsPath);
104 | }
105 |
106 | const fileStr = fs.readFileSync(presetsGlobalUri.fsPath, "utf8");
107 |
108 | const presets = parse(fileStr);
109 |
110 | // Tip: Install the es6-string-html VS Code extension to enable code highlighting below
111 | return /*html*/ `
112 |
113 |
114 |
115 | TestGPT
116 |
117 |
118 |
119 |
124 |
125 |
126 |
127 |
128 |
129 | `;
130 | } catch (err) {
131 | window.showErrorMessage(JSON.stringify(err));
132 | return JSON.stringify(err);
133 | }
134 | }
135 |
136 | /**
137 | * Sets up an event listener to listen for messages passed from the webview context and
138 | * executes code based on the message that is recieved.
139 | *
140 | * @param webview A reference to the extension webview
141 | * @param context A reference to the extension context
142 | */
143 | private _setWebviewMessageListener(webview: Webview) {
144 | webview.onDidReceiveMessage(
145 | async (message: any) => {
146 | if (message.type === "preset") {
147 | const presetsUri = getUri(webview, this.context.globalStorageUri, ["presets.yaml"]);
148 | const presets = parse(fs.readFileSync(presetsUri.fsPath, "utf8"));
149 | const presetIndex = presets.findIndex((p: any) => p.name === message.data.name);
150 |
151 | if (presetIndex === -1) {
152 | presets.push(message.data);
153 | } else if (message.data) {
154 | presets[presetIndex] = message.data;
155 | } else {
156 | presets.splice(presetIndex, 1);
157 | }
158 |
159 | const strPresets = stringify(presets);
160 |
161 | fs.writeFileSync(presetsUri.fsPath, strPresets);
162 | } else if (message.type === "test") {
163 | // get active file path
164 | const activeFilePath = window.visibleTextEditors[0]?.document.fileName;
165 | if (!activeFilePath) {
166 | window.showErrorMessage("No active file");
167 | return;
168 | }
169 |
170 | // use input file name only
171 | const inputFile = activeFilePath.split("/").pop();
172 |
173 | // if input file is a test file, show error message
174 | if (inputFile?.includes(".spec.") || inputFile?.includes(".test.")) {
175 | window.showErrorMessage("Cannot generate tests from test file");
176 | return;
177 | }
178 |
179 | const defaultOutputFile = activeFilePath.replace(/\.[^/.]+$/, (ext) => `.spec${ext}`);
180 | const outputFilePath = message.data.outputFile || defaultOutputFile;
181 | const outputFile = outputFilePath.split("/").pop();
182 |
183 | const stringifyData = (data: any) => {
184 | const unescaped = JSON.stringify(data);
185 |
186 | // escape anything needed so that it can be passed as a CLI command
187 | return unescaped;
188 | };
189 |
190 | const apiKey = workspace.getConfiguration().get("testgpt.apiKey");
191 |
192 | if (!apiKey) {
193 | const inputApiKey =
194 | process.env.OPENAI_API_KEY ||
195 | (await window.showInputBox({
196 | prompt: "Enter your OpenAI API Key:",
197 | ignoreFocusOut: true,
198 | }));
199 |
200 | if (!inputApiKey) {
201 | return window.showErrorMessage("OpenAI API Key is required");
202 | }
203 |
204 | await workspace
205 | .getConfiguration()
206 | .update("testgpt.apiKey", inputApiKey, ConfigurationTarget.Global);
207 | }
208 |
209 | const model = message.data.model;
210 | const streaming = message.data.streaming;
211 | const systemMessage = stringifyData(message.data.systemMessage);
212 | const promptTemplate = stringifyData(message.data.promptTemplate);
213 | const instructions = stringifyData(message.data.instructions);
214 | const techs = message.data.autoTechs ? "" : stringifyData(message.techs.join(", "));
215 | const examples = message.data.examples?.length && stringifyData(message.data.examples);
216 | const key = workspace.getConfiguration().get("testgpt.apiKey");
217 |
218 | if (!model || !outputFile || !systemMessage || !promptTemplate || !key) {
219 | console.error("Missing required fields");
220 | window.showErrorMessage("Missing required fields");
221 | return;
222 | }
223 |
224 | // Initialize an array to hold the command and its arguments
225 | let args = ["--yes", "testgpt@latest"];
226 |
227 | if (inputFile) args.push("-i", inputFile);
228 | if (outputFile) args.push("-o", outputFile);
229 | if (model) args.push("-m", model);
230 | if (key) args.push("-k", key);
231 | if (systemMessage) args.push("-y", systemMessage);
232 | if (promptTemplate) args.push("-p", promptTemplate);
233 | if (techs) args.push("-t", techs);
234 | if (examples) args.push("-x", examples);
235 | if (streaming) args.push("-s");
236 | if (instructions) args.push("-n", instructions);
237 |
238 | // create output file if it doesn't exist
239 | if (!fs.existsSync(outputFilePath)) {
240 | fs.writeFileSync(outputFilePath, "");
241 | }
242 |
243 | const outputFileDir = outputFilePath.split("/").slice(0, -1).join("/");
244 |
245 | const child = spawn("npx", args, { cwd: outputFileDir });
246 |
247 | if (streaming) {
248 | window.showTextDocument(Uri.file(outputFilePath));
249 | }
250 | }
251 | },
252 | undefined,
253 | this._disposables
254 | );
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Your AI testing companion that writes tests on your behalf, automated to get you to build and ship faster without sacrificing tests.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 | By default, TestGPT will use the OpenAI gpt-3.5-turbo-16k model, but you can use gpt-4, or any other model you want.
15 |
16 |
17 |
18 | Installation
19 |
20 |
21 |
22 | VSCode Extension
23 |
24 |
25 | An extension is available on the [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=FayezNazzal.testgpt)
26 |
27 | or install directly by entering this command in the VSCode command palette (Command+P) / (Ctrl+P):
28 |
29 | ```
30 | ext install fayeznazzal.testgpt
31 | ```
32 |
33 |
34 | CLI Tool
35 |
36 |
37 | To install the CLI tool, follow those steps
38 |
39 | 1. Install TestGPT by running one of these commands:
40 |
41 | ```zsh
42 | # Install globally
43 | npm install -g testgpt@latest
44 |
45 | # OR install locally in your project
46 | npm install testgpt@latest
47 | ```
48 |
49 | 2. **Get your OpenAI API Key** by requesting access to the [OpenAI API](https://openai.com/api/) and obtaining your [API key](https://platform.openai.com/account/api-keys).
50 |
51 | Then export it based on your OS:
52 |
53 | - **macOS or Linux:** Add the following line to .zshrc or .bashrc in your home directory:
54 |
55 | ```zsh
56 | export OPENAI_API_KEY="Your OpenAI API Key."
57 | ```
58 |
59 | Then run the command:
60 |
61 | ```zsh
62 | source ~/.zshrc
63 | ```
64 |
65 | - **Windows:** Go to System -> Settings -> Advanced -> Environment Variables, click New under System Variables, and create a new entry with the key `OPENAI_API_KEY` and your OpenAI API Key as the value.
66 |
67 |
68 | Usage
69 |
70 |
71 | ###
Universal / Plug and Play
72 |
73 | Here's a simple form of a test generation command:
74 |
75 | ```zsh
76 | testgpt -i ./component.tsx -m gpt4
77 | # Creates: ./component.test.tsx
78 | ```
79 |
80 | With more options, comes more power! You can easily specify target techs, tips, and specify a custom GPT model, along with other options. Here is a breakdown table:
81 |
82 |
83 |
84 |
85 | --inputFile
86 | |
87 | -i
88 | |
89 | [ Required ]
90 |
91 |
92 |
93 |
94 | Path for the input file to be tested (e.g. `./Button.tsx`).
95 |
96 |
97 |
98 |
99 |
100 | --outputFile
101 | |
102 | -o
103 | |
104 | [ Default: {inputFile}.test.{extension} ]
105 |
106 |
107 |
108 |
109 | Path for the output file where the generated tests will be written (e.g. `./Button.spec.tsx`). If not provided, the output file will be the same as the input file, but with `.test` added before the extension.
110 |
111 |
112 |
113 |
114 |
115 | --apiKey
116 | |
117 | -k
118 | |
119 | [ Default: OPENAI_API_KEY Env ]
120 |
121 |
122 |
123 |
124 | OpenAI API key. If not provided, it will be taken from the `OPENAI_API_KEY` environment variable. If using an API other than OpenAI, currently, this option will be ignored.
125 |
126 |
127 |
128 |
129 |
130 | --model
131 | |
132 | -m
133 | |
134 | [ Default: gpt-3.5-turbo-16k ]
135 |
136 |
137 |
138 |
139 | GPT model to be used for generating tests. If using an API other than OpenAI, currently, this option will be ignored.
140 |
141 |
142 |
143 |
144 |
145 | --stream
146 | |
147 | -s
148 |
149 |
150 |
151 |
152 |
153 | Stream the response using OpenAI streaming feature. If using an API other than OpenAI, currently, this option will be ignored.
154 |
155 |
156 |
157 |
158 |
159 | --systemMessage
160 | |
161 | -y
162 |
163 |
164 |
165 | System message to be used for generating tests.
166 |
167 |
168 |
169 |
170 |
171 | --promptTemplate
172 | |
173 | -p
174 |
175 |
176 |
177 | Prompt template to be used for generating tests. You can substitute the following variables in the template:
178 |
179 | - fileName: The name of the file being tested.
180 | - content: The content of the file being tested.
181 | - techs: The technologies to be used.
182 | - instructions: General Instructions for generating tests.
183 |
184 |
185 | To substitute a variable, use the following syntax: `{variableName}`
186 |
187 | Here is an example:
188 |
189 | ```js
190 | Please provide unit tests for the file {fileName} using {techs}
191 | {instructions}
192 |
193 | Please begin your response with \`\`\` and end it with \`\`\` directly.
194 |
195 | Here is the file content:
196 | \`\`\`{content}\`\`\`
197 | ```
198 |
199 |
200 |
201 |
202 |
203 |
204 | --techs
205 | |
206 | -t
207 | |
208 | [ Default: Auto Detected ]
209 |
210 |
211 |
212 | The technologies to be used.
213 |
214 |
215 |
216 |
217 |
218 | --examples
219 | |
220 | -e
221 | |
222 |
223 |
224 |
225 | Example snippets to guide the AI test generation process.
226 |
227 |
228 |
229 |
230 |
231 | --moduleEndpoint
232 | |
233 | -e
234 | |
235 |
236 |
237 |
238 | An API endpoint for a custom model to send the request to. Only use this if you have a custom model deployed and you want to use it instead of OpenAI.
239 |
240 |
241 |
242 |
243 |
244 | --instructions
245 | |
246 | -n
247 |
248 |
249 |
250 |
251 | General Instructions for generating tests.
252 |
253 |
254 |
255 |
256 |
257 | --config
258 | |
259 | -c
260 |
261 |
262 |
263 |
264 | Path to config file.
265 |
266 |
267 |
268 |
269 |
270 |
271 | Here is an example command that uses more options like those mentioned above:
272 |
273 | ```zsh
274 | testgpt -i ./Button.tsx -o ./Button.spec.tsx -m gpt-4 --techs "jest, testing-library" --apiKey "Your OpenAI API Key"
275 | ```
276 |
277 | ###
Locally / Config-based
278 |
279 | For extra flexibility, having `testgpt.config.yaml` at your project's root allows for running shorter commands, quicker, and more friendly for repetitive usage.
280 |
281 | An example of a `testgpt.config.yaml` file:
282 |
283 | ```yaml
284 | .tsx:
285 | techs:
286 | - jest
287 | - react-testing-library
288 | instructions: |-
289 | Wrap test groups in 'describe' blocks
290 | examples:
291 | - fileName: file1.tsx
292 | code:
293 | tests:
294 | - fileName: file2.tsx
295 | code:
296 | tests:
297 | ```
298 |
299 | > More and longer examples enhance the test quality. This will be more possible with high-context length models like gpt-3.5-turbo-16k or gpt-4-32k.
300 |
301 | ##
License
302 |
303 | This software is licensed under the MIT License, which permits you to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software, subject to the following conditions:
304 |
305 | - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the software.
306 | - The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement.
307 | - In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.
308 |
309 | Please feel free to use this software in any way you see fit, and contributions are always welcome :)
310 |
--------------------------------------------------------------------------------
/vscode/webview-ui/src/lib/Logo.svelte:
--------------------------------------------------------------------------------
1 |
282 |
--------------------------------------------------------------------------------
/media/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
270 |
--------------------------------------------------------------------------------
/vscode/media/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
270 |
--------------------------------------------------------------------------------