工具类-VSCode插件之gpt生成单测

1,352 阅读4分钟

AI热潮

从2023年3月OpenAI发布了GPT-4后,行业内掀起了一股AI浪潮,涌现了很多的AI工具,GitHub Copilot、通义灵码、Codeium等;工具功能也都大同小异,生成单测、代码解释、调优建议等。

最终效果

VScode插件+gpt单个/批量生成单测

VSCode插件

刚好学习怎么开发VSCode插件,不感兴趣可直接跳到AI生成单测模块。

插件项目

具体可查看文档:VSCode插件生成文档vscode-extension-samples

1、安装必要的工具,YeomanVS Code Extension Generator
sudo npm i -g yo generator-code
2、脚手架生成插件项目
yo code

3、项目分析

webpack.config.js文件中,配置了入口是src目录下的extension文件,打包后会生成dist/extension.js;

package.json中,main配置的就是./dist/extension.js

4、调试

两种方式:

  • 快捷键F5
  • 点击 run and debug

会新打开一个窗口;按cmd+shift+p,输入Hello World,底部会有提示弹窗。支持代码添加断点,进行调试;

Q&A:找不到 Hello World 命令

在新窗口,按cmd+shift+p,输入Hello World,发现找不到命令

这个问题是因为 vscode 版本不一致造成的

package.json 文件中指定的 vscode 版本号高于本地版本,可修改版本或者更新vscode到最近版。

5、配置命令

默认带commands,可配置explorer/context,当文件右键时,会有快捷方式。

更改命令名,可修改title

// package.json
"contributes": {
    "commands": [
      {
        "command": "ggt.helloWorld",
        "title": "Hello World"
      }
    ],
    "menus": {
      "explorer/context": [
        {
          "command": "ggt.helloWorld",
          "group": "navigation"
        }
      ]
    }
  },
  
  // extension.ts
  vscode.commands.registerCommand('ggt.helloWorld', () => {...})

6、发布
npm install vsce -g

// 登录,token: bvgzjoloaeo7a7qwlnptge54ydrinlhcqwg5qz3pvqt5e4t5paoq
vsce login wangsixiao

// 发布
vsce publish
// 可以在https://marketplace.visualstudio.com/manage/publishers/wangsixiao 查看发布的插件

AI生成单测

支持两种方式:

  • new openai
  • URL方式,使用axios请求

new openai

推荐一个能免费获取key的方式:GPT-API-free

点击即可获取到key,gpt-4有使用限制,3是完全免费的。

先上代码

openai使用文档:Chat Completions API

import OpenAI from "openai";
import * as vscode from "vscode";

// 由于我用的是上面的方法获取到的key,所以baseURL我配置的是他提供的转发host
const openai = new OpenAI({
  baseURL: 'https://api.chatanywhere.com.cn',
});

/**
 * 直接调用openai的方法
 * @param path 
 * @param code 
 * @returns 
 */
const UNIT_TEST_REQUEST = (path: string, code: string) =>
  `Generate a unit test with the jest syntax, containing relevant assertions and required packages in a single 'describe' block. Import the functions from ${path} and use them to test the following code snippet:\n\n ${code}\n\n, 生成的内容只是单测代码,不要加其他的内容描述,确保测试用例能跑通,正确模拟reacthooks,正确模拟异步场景`;

export async function generateUnitTests(path: string, code: string) {
  try {
    const response = await openai.chat.completions.create({
      // model: "gpt-3.5-turbo",
      model: "gpt-4",
      messages: [
        {
          role: "user",
          content: UNIT_TEST_REQUEST(path, code),
        },
      ],
      temperature: 0,
    });
    return response.choices[0].message.content
  }catch(error) {
    vscode.window.showErrorMessage(
      `Error generating unit tests from AI because of: ${error}`
    );
  }
}
1、baseURL

默认取的是process.env['OPENAI_BASE_URL'];

由于我用的是上面的方法获取到的key,所以baseURL我配置的是他提供的转发host;不配置,会有如下报错

2、Mac 配置 AI 环境变量 - process.env['OPENAI_API_KEY']

本文档只写了mac版本的配置步骤,windows可看详细的文档步骤:MacOS+NodeJS配置步骤

apiKey 支持传入,默认取环境里的OPENAI_API_KEY;

为了保证我们的key不被别人窃取,一般是配置在环境变量里。

配置步骤:

先判断shell是哪个应用。

echo $0 // 返回 -zsh 或 -bash

额外知识点:环境变量分为全局变量和用户变量;用户变量的配置文件由具体shell应用决定。

zsh版本

// 在Terminal执行命令, 打开配置文件
nano ~/.zshrc

// 配置OPENAI_API_KEY变量
export OPENAI_API_KEY='your-api-key'

// 执行下面的命令,保存&退出
Ctrl+O
Ctrl+X

// 执行source让变量生效
source ~/.zshrc

// 输入下面命令,看是否生效,如果返回你配置的值表示成功啦
echo $OPENAI_API_KEY // sk-xxxx

bash版本

// 在Terminal执行命令, 打开配置文件
nano ~/.bash_profile

// 配置OPENAI_API_KEY变量
export OPENAI_API_KEY='your-api-key sk-xxx'

// 执行下面的命令,保存&退出
Ctrl+O
Ctrl+X

// 执行source让变量生效
source ~/.bash_profile

// 输入下面命令,看是否生效,如果返回你配置的值表示成功啦
echo $OPENAI_API_KEY // sk-xxxx

axios请求

import axios from "axios";

/**
 * URL方式,使用axios请求
 * @param path 
 * @param code 
 * @returns 
 */
export async function generateUnitTestsAxios(path: string, code: string) {
  try {
    const params = new URLSearchParams();
    // 接口参数配置,主要看接口都需要什么参数,是啥形式的
    params.append("currentUserId", "0");
    params.append(
      "request",
      JSON.stringify({
        model: "gpt-4",
        messages: [
            {
              role: "user",
              content: UNIT_TEST_REQUEST(path, code),
            },
        ]
      })
    );
    // 请求配置
    const response = await axios({
      url: "xxx",
      timeout: 60000,
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "X-Requested-With": "XMLHttpRequest",
      },
      responseType: "json",
      data: params,
    });
    return response?.data?.data?.choices?.[0].message?.content;
  } catch (error) {
    vscode.window.showErrorMessage(
      `Error generating unit tests from AI because of: ${error}`
    );
    throw new Error(`Error generating unit tests from AI because of: ${error}`);
  }
}

批量生成

判断选中要生成单测的resource,如果是文件夹,则需要批量

const fileResource = await vscode.workspace.fs.stat(resource);

if (fileResource.type === vscode.FileType.Directory) {
  // 批量生成
  await traverseFolder(resource.fsPath);
} else {
  // 单个生成
  await singleFileGeneration(
    vscode.window.activeTextEditor?.document.fileName
  );
}

遍历文件夹,递归处理文件夹下所有符合需要生成单测的文件,调用单个文件生成单测方法

/**
 * 遍历文件夹
 * @param folderPath
 * @returns
 */
export const traverseFolder = async (folderPath = "") => {
  if (!folderPath) return;
  const files = fs.readdirSync(folderPath);
  for (let file of files) {
    const filePath = path.join(folderPath, file);
    const stats = fs.lstatSync(filePath);
    if (stats.isDirectory()) {
      traverseFolder(filePath); // 递归遍历子文件夹
    } else {
      if (supportFileTypes(file.split(".")?.[1])) {
        await singleFileGeneration(filePath);
      }
    }
  }
};

单个文件生成单测

/**
 * 单个文件单测生成
 * @param filePath
 * @returns
 */
export const singleFileGeneration = async (filePath = "") => {
  if (!filePath) return;
  const fileParts: Array<string> = filePath ? filePath.split("/") : [];
  const fileName: string = fileParts[fileParts.length - 1];
  fileParts.pop();
  const currentDirectory = fileParts.join("/");
  if (filePath && supportFileTypes(fileName.split(".")?.[1])) {
    // 获取文件内容
    const code = await readFileContents(filePath);
    if (code) {
      // 生成单测
      const content = await generateUnitTests(filePath, code);
      // 写入文件
      content && (await writeToFile(currentDirectory, fileName, content));
    }
  } else {
    vscode.window.showErrorMessage(
      "Not a valid file for unit test generation."
    );
  }
};