-
Notifications
You must be signed in to change notification settings - Fork 258
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: test (edit) command & add tests code lenses (#2959)
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
Showing
39 changed files
with
1,635 additions
and
1,513 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | ||
} | ||
} |
Oops, something went wrong.