Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support python and ruby dependency files as well as package.json #39

Merged
merged 6 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/helpers/dependency-files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, it, expect, vi } from 'vitest';
import { getDependencyFile, getDependencyFileName } from './dependency-files';

const mocks = vi.hoisted(() => {
return {
readFile: vi.fn(),
fileExists: vi.fn(),
};
});

vi.mock('fs/promises', () => {
return {
readFile: mocks.readFile,
};
});

vi.mock('./file-exists', () => {
return {
fileExists: mocks.fileExists,
};
});

describe('getDependencyFile', () => {
it('should return the contents of package.json for node', async () => {
const packageJsonContent = JSON.stringify({
name: 'example',
version: '1.0.0',
});
mocks.readFile.mockResolvedValueOnce(packageJsonContent);
mocks.fileExists.mockResolvedValue(true);

const result = await getDependencyFile('', 'ts');
expect(result).toBe(packageJsonContent);
expect(mocks.readFile).toHaveBeenCalledWith('package.json', 'utf8');
});

it('should return the contents of requirements.txt for python', async () => {
const requirementsTxtContent = 'example-package==1.0.0';
mocks.readFile.mockResolvedValueOnce(requirementsTxtContent);
mocks.fileExists.mockResolvedValue(true);

const result = await getDependencyFile('', 'py');
expect(result).toBe(requirementsTxtContent);
expect(mocks.readFile).toHaveBeenCalledWith('requirements.txt', 'utf8');
});

it('should return the contents of Gemfile for ruby', async () => {
const gemfileContent = "gem 'rails', '5.0.0'";
mocks.readFile.mockResolvedValueOnce(gemfileContent);
mocks.fileExists.mockResolvedValue(true);

const result = await getDependencyFile('', 'rb');
expect(result).toBe(gemfileContent);
expect(mocks.readFile).toHaveBeenCalledWith('Gemfile', 'utf8');
});

it('should check all three dependency files if no language is provided', async () => {
mocks.fileExists.mockReset();
mocks.fileExists.mockResolvedValue(false);

const result = await getDependencyFile('/src');
expect(mocks.fileExists).toHaveBeenCalledTimes(3);
expect(mocks.fileExists).toHaveBeenCalledWith('/src/package.json');
expect(mocks.fileExists).toHaveBeenCalledWith('/src/requirements.txt');
expect(mocks.fileExists).toHaveBeenCalledWith('/src/Gemfile');
});

it('should return null if package.json file does not exist', async () => {
mocks.fileExists.mockResolvedValue(false);

await expect(getDependencyFile()).resolves.toBeNull();
});

it('should return null if requirements.txt does not exist for python', async () => {
mocks.fileExists.mockResolvedValue(false);

await expect(getDependencyFile(process.cwd(), 'py')).resolves.toBeNull();
});

it('should return null if Gemfile does not exist for ruby', async () => {
mocks.fileExists.mockResolvedValue(false);

await expect(getDependencyFile(process.cwd(), 'rb')).resolves.toBeNull();
});
});

describe('getDependencyFileName', () => {
it('should return package.json for node', () => {
const result = getDependencyFileName();
expect(result).toBe('package.json');
});

it('should return requirements.txt for python', () => {
const result = getDependencyFileName('py');
expect(result).toBe('requirements.txt');
});

it('should return Gemfile for ruby', () => {
const result = getDependencyFileName('rb');
expect(result).toBe('Gemfile');
});
});
68 changes: 68 additions & 0 deletions src/helpers/dependency-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { fileExists } from './file-exists';

/**
* Find dependency file in the given directory or any
* parent directory. Returns the content of the dependency file.
*/
export async function getDependencyFile(
directory = process.cwd(),
language?: string
): Promise<string | null> {
let currentDirectory = directory;
while (currentDirectory !== '/') {
if (language) {
const filePath = getDependenciesFilePath(directory, language);

if (await fileExists(filePath)) {
return getDependenciesFileContent(directory, language);
}
} else {
let filePath = getDependenciesFilePath(directory, 'py');
if (await fileExists(filePath)) {
return getDependenciesFileContent(directory, 'py');
}
filePath = getDependenciesFilePath(directory, 'rb');
if (await fileExists(filePath)) {
return getDependenciesFileContent(directory, 'rb');
}
filePath = getDependenciesFilePath(directory, 'js');
if (await fileExists(filePath)) {
return getDependenciesFileContent(directory, 'js');
}
}
currentDirectory = path.dirname(currentDirectory);
}
return null;
}

export function getDependencyFileName(language?: string): string {
let fileName;
switch (language) {
case 'py':
fileName = 'requirements.txt';
break;
case 'rb':
fileName = 'Gemfile';
break;
default:
fileName = 'package.json';
break;
}
return fileName;
}

function getDependenciesFilePath(directory: string, language?: string): string {
const fileName = getDependencyFileName(language);
return path.join(directory, fileName);
}

async function getDependenciesFileContent(
directory = process.cwd(),
language?: string
): Promise<string> {
const filePath = getDependenciesFilePath(directory, language);
const fileContent = await fs.readFile(filePath, 'utf8');
return fileContent;
}
21 changes: 0 additions & 21 deletions src/helpers/find-package-json.ts
Original file line number Diff line number Diff line change
@@ -1,21 +0,0 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { fileExists } from './file-exists';

/**
* Find package.json file in the given directory or any
* parent directory.
*/
export async function findPackageJson(
directory = process.cwd()
): Promise<string | null> {
let currentDirectory = directory;
while (currentDirectory !== '/') {
const packageJsonPath = path.join(currentDirectory, 'package.json');
if (await fileExists(packageJsonPath)) {
return fs.readFile(packageJsonPath, 'utf-8');
}
currentDirectory = path.dirname(currentDirectory);
}
return null;
}
31 changes: 22 additions & 9 deletions src/helpers/get-test-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import dedent from 'dedent';
import { findPackageJson } from './find-package-json';
import { getDependencyFile, getDependencyFileName } from './dependency-files';
import { getSimpleCompletion } from './llm';
import { removeBackticks } from './remove-backticks';

Expand All @@ -12,8 +12,13 @@ export async function getTestCommand({
testFilePath.split('/').pop()!.split('.')[0]
}`;

const packageJson = await findPackageJson();
if (!packageJson) {
const testFileExtension = testFilePath.split('.').pop();
const dependencyFileName = getDependencyFileName(testFileExtension);
const dependencyFileContent = await getDependencyFile(
process.cwd(),
testFileExtension
);
if (!dependencyFileContent) {
return defaultTestCommand;
}

Expand All @@ -28,23 +33,31 @@ export async function getTestCommand({
{
role: 'user',
content: dedent`
Here is my package.json. I want to run a single command to execute the tests. The tests should not run in watch mode.
If there is a test script in the package.json, use that script. For example, \`npm test\`.
Here is my ${dependencyFileName}. I want to run a single command to execute the tests. The tests should not run in watch mode.
If there is a test script in the ${dependencyFileName}, use that script. For example, \`npm test\`.
The command should filter and run the specific test file at \`${testFilePath}\`. For example, \`npm test -- ${
testFilePath.split('/').pop()!.split('.')[0]
}\`.

Here are sample npm test commands without watch mode that work for some popular testing libraries:
Here are sample test commands without watch mode that work for some popular testing libraries:
- Jest: \`npm test -- ${
testFilePath.split('/').pop()!.split('.')[0]
} --no-watch \`
- Vitest: \`npm test -- ${
testFilePath.split('/').pop()!.split('.')[0]
} --run \`
- minitest: \`rails test ${
testFilePath.split('/').pop()!.split('.')[0]
}\`
- rspec: \`rspec ${testFilePath.split('/').pop()!.split('.')[0]}\`
- pytest: \`pytest ${testFilePath.split('/').pop()!.split('.')[0]}\`
- unittest: \`python -m unittest ${
testFilePath.split('/').pop()!.split('.')[0]
}\`

<package-json>
${packageJson}
</package-json>
<${dependencyFileName.replace('.', '-')}>
${dependencyFileContent}
</${dependencyFileName.replace('.', '-')}>

If no testing libraries are found in the package.json, use \`npx vitest run ${
testFilePath.split('/').pop()!.split('.')[0]
Expand Down
24 changes: 3 additions & 21 deletions src/helpers/validate-project.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
import fs from 'fs/promises';
import path from 'path';
import { findPackageJson } from './find-package-json';
import { getDependencyFile } from './dependency-files';

export async function isValidProject(): Promise<boolean> {
const currentDir = process.cwd();
const requirementsTxtPath = path.join(currentDir, 'requirements.txt');

try {
await findPackageJson();
return true;
} catch {
// Do nothing if file doesn't exist
}

try {
await fs.access(requirementsTxtPath);
return true;
} catch {
// Do nothing if file doesn't exist
}

return false;
const fileContent = await getDependencyFile();
return fileContent !== null;
}
Loading