Skip to content

Commit

Permalink
feat: test (edit) command & add tests code lenses (#2959)
Browse files Browse the repository at this point in the history
CLOSE #1475 &
#1761

PR to introduce `/unit` command that is a smarter version of the `/test`
command.

### /unit command

- `/unit` command generates unit tests in chat (old `/test`)
- `/test` command generates unit tests in line
- if a test file exists, the new unit test suit will be appended to the
end of the current test file
- if a test file does not exist, cody will suggest a file name and we
will create a temporary test file with the new test suit appended along
with the import statements (if any)


![unit-test-file](https://github.com/sourcegraph/cody/assets/68532117/0bac781c-ee69-4e30-9697-9b891944a7b7)

### Add Tests code lenses

`add-tests` is a command that can be executed via the Cody Command
CodeLenses (when enabled) in test files. It is not available via the
command menu or context menu.

Behind internal unstable flag. 


![unit-test-case](https://github.com/sourcegraph/cody/assets/68532117/4eef46d6-7165-4cf5-8ad6-b0e555263052)

## Summary

This PR includes the following change:
- Refactor the /unit command, rename it to /test, and move it out of
unstable
- Rename the old /test command to /unit
- Add test-case command
- Add `executeUnitCaseCommand` to execute unit test cases
- Added support for specifying a `destination file` in the
executeEditArguments interface in the edit/execute.ts file.
- Added support for specifying a `destination file` in the executeEdit
method in the edit/manager.ts file.
- Removed the `NewFixupFileMap` import and added support for specifying
a `destination file` in the EditProvider class in the edit/provider.ts
file.
- Removed the `NewFixupFileMap` import in the FixupController class in
the non-stop/FixupController.ts file.

## Test plan

<!-- Required. See
https://sourcegraph.com/docs/dev/background-information/testing_principles.
-->

Added e2e test.

### Test

Run the `Test` command to confirm the newly generated tests are added to
a new / exisiting test file automatically for you.


![image](https://github.com/sourcegraph/cody/assets/68532117/a04a61b7-7a6b-485c-af76-d584f379b959)

### Add Tests

Enable Code Lenses in your Cody Setting


![image](https://github.com/sourcegraph/cody/assets/68532117/79a4ceaa-5f56-4c3a-9a33-0770d3057a74)

Locate the `Add Tests` code lenses in a test file to check if new test
cases have been inserted in line for you


![image](https://github.com/sourcegraph/cody/assets/68532117/c0a09081-a36e-4167-9ef0-e65fef60d0b7)


## Follow-up

- [ ] option to append to current selection instead of insert
- [ ] context improvement for test

---------

Co-authored-by: Tom Ross <tom@umpox.com>
  • Loading branch information
abeatrix and umpox committed Feb 1, 2024
1 parent 70e927c commit 0513f7a
Show file tree
Hide file tree
Showing 39 changed files with 1,635 additions and 1,513 deletions.
4 changes: 2 additions & 2 deletions lib/shared/src/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ export type DefaultCodyCommands = DefaultChatCommands | DefaultEditCommands
// Default Cody Commands that runs as a Chat request
export enum DefaultChatCommands {
Explain = 'explain', // Explain code
Test = 'test', // Generate unit tests in Chat
Unit = 'unit', // Generate unit tests in Chat
Smell = 'smell', // Generate code smell report in Chat
}

// Default Cody Commands that runs as an Inline Edit command
export enum DefaultEditCommands {
Unit = 'unit', // Generate unit tests with inline edit
Test = 'test', // Generate unit tests with inline edit
Doc = 'doc', // Generate documentation with inline edit
}

Expand Down
2 changes: 2 additions & 0 deletions vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This is a log of all notable changes to Cody for VS Code. [Unreleased] changes a
- Edit/Chat: Added "ghost" text alongside code to showcase Edit and Chat commands. Enable it by setting `cody.commandHints.enabled` to true. [pull/2865](https://github.com/sourcegraph/cody/pull/2865)
- Autocomplete: local inference support with [deepseek-coder](https://ollama.ai/library/deepseek-coder) powered by ollama. [pull/2966](https://github.com/sourcegraph/cody/pull/2966)
- Autocomplete: Add a new experimental fast-path mode for Cody community users that directly connections to our inference services. [pull/2927](https://github.com/sourcegraph/cody/pull/2927)
- Command: The `Generate Unit Tests` command now functions as an inline edit command. When executed, the new tests will be automatically appended to the test file. If no existing test file is found, a temporary one will be created. [pull/2959](https://github.com/sourcegraph/cody/pull/2959)
- [Internal] Command: Added new code lenses for generating additional unit tests. [pull/2959](https://github.com/sourcegraph/cody/pull/2959)

### Fixed

Expand Down
2,204 changes: 1,078 additions & 1,126 deletions vscode/package.json

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions vscode/src/commands/context/unit-test-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ContextFile } from '@sourcegraph/cody-shared'

import { isTestFileForOriginal, isValidTestFile } from '../utils/test-commands'
import { getWorkspaceFilesContext } from './workspace'
import { getSearchPatternForTestFiles } from '../utils/search-pattern'
import type { URI } from 'vscode-uri'
import { getContextFileFromDirectory } from './directory'

export async function getContextFilesForAddingUnitTestCases(testFile: URI): Promise<ContextFile[]> {
// Get the context from the current directory
// and then find the original file of the test file in the returned context
// If the original file is found, return it
// e.g. if the test file is src/foo/bar.spec.ts, look for src/foo/bar.ts
const directoryContext = await getContextFileFromDirectory()
const originalFileContext = directoryContext.find(f => isTestFileForOriginal(f.uri, testFile))
if (originalFileContext) {
return [originalFileContext]
}

// TODO (bee) improves context search
const contextFiles: ContextFile[] = []
// exclude any files in the path with e2e, integration, node_modules, or dist
const excludePattern = '**/*{e2e,integration,node_modules,dist}*/**'
// To search for files in the current directory only
const searchInCurrentDirectoryOnly = true
// The max number of files to search for in each workspace search
const max = 10

// Search for test files in the current directory first
const curerntDirPattern = getSearchPatternForTestFiles(testFile, searchInCurrentDirectoryOnly)
const currentDirContext = await getWorkspaceFilesContext(curerntDirPattern, excludePattern, max)

contextFiles.push(...currentDirContext)

// If no test files found in the current directory, search the entire workspace
if (!contextFiles.length) {
const wsTestPattern = getSearchPatternForTestFiles(testFile, !searchInCurrentDirectoryOnly)
// Will try to look for half the max number of files in the workspace for faster results
const codebaseFiles = await getWorkspaceFilesContext(wsTestPattern, excludePattern, max / 2)

contextFiles.push(...codebaseFiles)
}

// Return valid test files only
return contextFiles.filter(f => isValidTestFile(f.uri))
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ export async function getContextFilesForUnitTestCommand(file: URI): Promise<Cont
// To search for files in the current directory only
const searchInCurrentDirectoryOnly = true
// The max number of files to search for in each workspace search
const max = 5
const max = 10

// Search for test files in the current directory first
const curerntDirPattern = getSearchPatternForTestFiles(file, searchInCurrentDirectoryOnly)
contextFiles.push(...(await getWorkspaceFilesContext(curerntDirPattern, excludePattern, max)))
const currentDirContext = await getWorkspaceFilesContext(curerntDirPattern, excludePattern, max)

contextFiles.push(...currentDirContext)

// If no test files found in the current directory, search the entire workspace
if (!contextFiles.length) {
const wsTestPattern = getSearchPatternForTestFiles(file, !searchInCurrentDirectoryOnly)
const codebaseFiles = await getWorkspaceFilesContext(wsTestPattern, excludePattern, max)
// Will try to look for half the max number of files in the workspace for faster results
const codebaseFiles = await getWorkspaceFilesContext(wsTestPattern, excludePattern, max / 2)

contextFiles.push(...codebaseFiles)
}
Expand Down
13 changes: 1 addition & 12 deletions vscode/src/commands/execute/cody.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,14 @@
},
"test": {
"description": "Generate unit tests",
"prompt": "Review the shared code context and configurations to identify the test framework and libraries in use. Then, generate a suite of multiple unit tests for the functions in <selected> using the detected test framework and libraries. Be sure to import the function being tested. Follow the same patterns as any shared context. Only add packages, imports, dependencies, and assertions if they are used in the shared code. Pay attention to the file path of each shared context to see if test for <selected> already exists. If one exists, focus on generating new unit tests for uncovered cases. If none are detected, import common unit test libraries for {languageName}. Focus on validating key functionality with simple and complete assertions. Only include mocks if one is detected in the shared code. Before writing the tests, identify which test libraries and frameworks to import, e.g. 'No new imports needed - using existing libs' or 'Importing test framework that matches shared context usage' or 'Importing the defined framework', etc. Then briefly summarize test coverage and any limitations. At the end, enclose the full completed code for the new unit tests, including all necessary imports, in a single markdown codeblock. No fragments or TODO. The new tests should validate expected functionality and cover edge cases for <selected> with all required imports, including importing the function being tested. Do not repeat existing tests.",
"prompt": "Review the shared context to identify the testing framework and libraries in use. Then, generate a suite of multiple unit tests for the selected function using the detected test framework and libraries. Be sure to import the function being tested. Use the same patterns, testing conventions, and testing library as shown in the shared context. Only import modules, functions, dependencies, and mocks based on shared code. If a test suite for the selected code is in the shared context, focus on generating new tests for uncovered cases. If none are detected, import common unit test libraries for {languageName}. Focus on validating key functionality with simple and complete assertions. Before writing the tests, identify which testing libraries and frameworks to use and import. At the end, enclose the fully completed code for the new unit tests without any comments, fragments, or TODOs. The new tests should validate the expected functionality and cover edge cases for with all required imports, including the function being tested. Do not repeat tests from the shared context. Enclose only the complete runnable tests.",
"context": {
"currentDir": true,
"currentFile": true,
"selection": true
},
"type": "default"
},
"unit": {
"description": "Experimental: Generate unit tests in test file directly",
"prompt": "Review the shared code context and configurations to identify the testing framework and libraries in use. Then, generate a suite of multiple unit tests for the functions in <selected> using the detected test framework and libraries. Be sure to import the function being tested. Follow the same patterns, testing conventions and testing library as shown in shared context. Only add packages, imports, dependencies, and assertions if they are used in the shared code. Pay attention to the file name of each shared context to see if test for <selected> already exists. If one exists, focus on generating new unit tests for uncovered cases. If none are detected, import common unit test libraries for {languageName}. Focus on validating key functionality with simple and complete assertions. Only include mocks if one is detected in the shared code. Before writing the tests, identify which testing libraries and frameworks to use and import. Then, enclose the file name for the unit test file between <FILEPATH7041> tags. At the end, enclose the full completed code for the new unit tests without any comments, fragments or TODO. The new tests should validate expected functionality and cover edge cases for <selected> with all required imports, including the function being tested. Do not repeat tests in shared context. Do not include any markdown formatting or triple backticks. Enclose only the complete runnable tests. If there is a shared context that has the same test file name, only include import if no conflict exists.",
"context": {
"currentDir": true,
"currentFile": true,
"selection": true
},
"type": "experimental",
"mode": "file"
},
"smell": {
"description": "Find code smells",
"prompt": "Please review and analyze the selected code and identify potential areas for improvement related to code smells, readability, maintainability, performance, security, etc. Do not list issues already addressed in the given code. Focus on providing up to 5 constructive suggestions that could make the code more robust, efficient, or align with best practices. For each suggestion, provide a brief explanation of the potential benefits. After listing any recommendations, summarize if you found notable opportunities to enhance the code quality overall or if the code generally follows sound design principles. If no issues found, reply 'There are no errors.'",
Expand Down
17 changes: 9 additions & 8 deletions vscode/src/commands/execute/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ import {
} from '@sourcegraph/cody-shared/src/commands/types'
import { executeSmellCommand } from './smell'
import { executeExplainCommand } from './explain'
import { executeTestCommand } from './test'
import { executeTestChatCommand } from './test-chat'
import { executeDocCommand } from './doc'
import type { CommandResult } from '../../main'
import { executeUnitTestCommand } from './unit'
import { executeTestEditCommand } from './test-edit'

export { commands as defaultCommands } from './cody.json'

export { executeSmellCommand } from './smell'
export { executeExplainCommand } from './explain'
export { executeTestCommand } from './test'
export { executeTestChatCommand } from './test-chat'
export { executeDocCommand } from './doc'
export { executeUnitTestCommand } from './unit'
export { executeTestEditCommand } from './test-edit'
export { executeTestCaseEditCommand } from './test-case'

export function isDefaultChatCommand(id: string): DefaultChatCommands | undefined {
// Remove leading slash if any
Expand Down Expand Up @@ -51,10 +52,10 @@ export async function executeDefaultCommand(
return executeExplainCommand({ additionalInstruction })
case DefaultChatCommands.Smell:
return executeSmellCommand({ additionalInstruction })
case DefaultChatCommands.Test:
return executeTestCommand({ additionalInstruction })
case DefaultEditCommands.Unit:
return executeUnitTestCommand({ additionalInstruction })
case DefaultChatCommands.Unit:
return executeTestChatCommand({ additionalInstruction })
case DefaultEditCommands.Test:
return executeTestEditCommand({ additionalInstruction })
case DefaultEditCommands.Doc:
return executeDocCommand({ additionalInstruction })
default:
Expand Down
56 changes: 56 additions & 0 deletions vscode/src/commands/execute/test-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Range } from 'vscode'
import { logError, type ContextFile } from '@sourcegraph/cody-shared'
import { getEditor } from '../../editor/active-editor'
import { type ExecuteEditArguments, executeEdit } from '../../edit/execute'
import { DefaultEditCommands } from '@sourcegraph/cody-shared/src/commands/types'
import type { EditCommandResult } from '../../main'
import type { CodyCommandArgs } from '../types'
import { getContextFilesForAddingUnitTestCases } from '../context/unit-test-case'

/**
* Adds generated test cases to the selected test suite inline.
*
* NOTE: Used by Code Lenses in test files with 'cody.command.tests-cases'.
*/
export async function executeTestCaseEditCommand(
args?: Partial<CodyCommandArgs>
): Promise<EditCommandResult | undefined> {
const instruction =
'Review the shared code context to identify the testing framework and libraries in use. Then, create multiple new unit tests for the test suite in my selected code following the same patterns, testing conventions, and testing library as shown in the shared context. Pay attention to the shared context to ensure that your response code does not contain cases that have already been covered. Focus on generating new unit tests for uncovered cases. Respond only with the fully completed code with the new unit tests added at the end, without any comments, fragments, or TODO. The new tests should validate expected functionality and cover edge cases for the test suites. The goal is to provide me with code that I can add to the end of the existing test file. Do not repeat tests from the shared context. Enclose only the new tests without describe/suite, import statements, or packages in your response.'

const editor = getEditor()?.active
const document = editor?.document
// Current selection is required
if (!document || !editor.selection) {
return
}

const contextFiles: ContextFile[] = []

try {
const files = await getContextFilesForAddingUnitTestCases(document.uri)
contextFiles.push(...files)
} catch (error) {
logError('executeNewTestCommand', 'failed to fetch context', { verbose: error })
}

const startLine = editor.selection.start.line + 1
const endLine = Math.max(startLine, editor.selection.end.line - 1)
const range = new Range(startLine, 0, endLine, 0)

return {
type: 'edit',
task: await executeEdit(
{
instruction,
document,
range,
intent: 'edit',
mode: 'insert',
userContextFiles: contextFiles,
destinationFile: document.uri,
} satisfies ExecuteEditArguments,
DefaultEditCommands.Test
),
}
}
76 changes: 76 additions & 0 deletions vscode/src/commands/execute/test-chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { logError, type ContextFile, logDebug } from '@sourcegraph/cody-shared'
import { getEditor } from '../../editor/active-editor'
import { getContextFileFromCursor } from '../context/selection'
import type { CodyCommandArgs } from '../types'
import { type ExecuteChatArguments, executeChat } from './ask'
import type { ChatCommandResult } from '../../main'
import { getContextFilesForTestCommand } from '../context/unit-test-chat'
import { telemetryService } from '../../services/telemetry'
import { telemetryRecorder } from '../../services/telemetry-v2'
/**
* Generates the prompt and context files with arguments for the '/test' command in Chat.
*
* Context: Test files, current selection, and current file
*/
async function unitTestCommand(args?: Partial<CodyCommandArgs>): Promise<ExecuteChatArguments> {
let prompt =
"Review the shared code context and configurations to identify the test framework and libraries in use. Then, generate a suite of multiple unit tests for the functions in <selected> using the detected test framework and libraries. Be sure to import the function being tested. Follow the same patterns as any shared context. Only add packages, imports, dependencies, and assertions if they are used in the shared code. Pay attention to the file path of each shared context to see if test for <selected> already exists. If one exists, focus on generating new unit tests for uncovered cases. If none are detected, import common unit test libraries for {languageName}. Focus on validating key functionality with simple and complete assertions. Only include mocks if one is detected in the shared code. Before writing the tests, identify which test libraries and frameworks to import, e.g. 'No new imports needed - using existing libs' or 'Importing test framework that matches shared context usage' or 'Importing the defined framework', etc. Then briefly summarize test coverage and any limitations. At the end, enclose the full completed code for the new unit tests, including all necessary imports, in a single markdown codeblock. No fragments or TODO. The new tests should validate expected functionality and cover edge cases for <selected> with all required imports, including importing the function being tested. Do not repeat existing tests."

if (args?.additionalInstruction) {
prompt = `${prompt} ${args.additionalInstruction}`
}

const editor = getEditor()?.active
const document = editor?.document
const contextFiles: ContextFile[] = []

if (document) {
try {
const cursorContext = await getContextFileFromCursor()
contextFiles.push(...cursorContext)

contextFiles.push(...(await getContextFilesForTestCommand(document.uri)))
} catch (error) {
logError('testCommand', 'failed to fetch context', { verbose: error })
}
}

return {
text: prompt,
contextFiles,
addEnhancedContext: false,
source: 'test',
submitType: 'user-newchat',
}
}

/**
* Executes the /test command for generating unit tests in Chat for selected code.
*
* NOTE: Currently used by agent until inline test command is added to agent.
*/
export async function executeTestChatCommand(
args?: Partial<CodyCommandArgs>
): Promise<ChatCommandResult | undefined> {
logDebug('executeTestEditCommand', 'executing', { args })
telemetryService.log('CodyVSCodeExtension:command:test:executed', {
useCodebaseContex: false,
requestID: args?.requestID,
source: args?.source,
})
telemetryRecorder.recordEvent('cody.command.test', 'executed', {
metadata: {
useCodebaseContex: 0,
},
interactionID: args?.requestID,
privateMetadata: {
requestID: args?.requestID,
source: args?.source,
},
})

return {
type: 'chat',
session: await executeChat(await unitTestCommand(args)),
}
}
Loading

0 comments on commit 0513f7a

Please sign in to comment.