From e431444f955b17f1912c9673548c07084a540b56 Mon Sep 17 00:00:00 2001 From: hiroki osame Date: Mon, 27 Mar 2023 05:44:16 -0400 Subject: [PATCH] test: refactor to run asynchronously (#174) --- tests/index.ts | 2 +- tests/specs/cli.ts | 170 --------------------------------- tests/specs/cli/commits.ts | 137 ++++++++++++++++++++++++++ tests/specs/cli/error-cases.ts | 24 +++++ tests/specs/cli/index.ts | 8 ++ tests/specs/config.ts | 8 +- tests/utils.ts | 31 ++++-- 7 files changed, 193 insertions(+), 187 deletions(-) delete mode 100644 tests/specs/cli.ts create mode 100644 tests/specs/cli/commits.ts create mode 100644 tests/specs/cli/error-cases.ts create mode 100644 tests/specs/cli/index.ts diff --git a/tests/index.ts b/tests/index.ts index d5164092..e8aa8628 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,6 +1,6 @@ import { describe } from 'manten'; describe('aicommits', ({ runTestSuite }) => { - runTestSuite(import('./specs/cli.js')); + runTestSuite(import('./specs/cli/index.js')); runTestSuite(import('./specs/config.js')); }); diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts deleted file mode 100644 index fd3ebe18..00000000 --- a/tests/specs/cli.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { testSuite, expect } from 'manten'; -import { createFixture } from 'fs-fixture'; -import { createAicommits, createGit } from '../utils.js'; - -const { OPENAI_KEY } = process.env; - -export default testSuite(({ describe }) => { - if (process.platform === 'win32') { - // https://github.com/nodejs/node/issues/31409 - console.warn('Skipping tests on Windows because Node.js spawn cant open TTYs'); - return; - } - - if (!OPENAI_KEY) { - console.warn('⚠️ process.env.OPENAI_KEY is necessary to run these tests. Skipping...'); - return; - } - - describe('CLI', async ({ test }) => { - const data: Record = { - firstName: 'Hiroki', - }; - const fixture = await createFixture({ - 'data.json': JSON.stringify(data), - }); - - const aicommits = createAicommits({ - cwd: fixture.path, - home: fixture.path, - }); - - await test('Fails on non-Git project', async () => { - const { stdout, exitCode } = await aicommits([], { reject: false }); - expect(exitCode).toBe(1); - expect(stdout).toMatch('The current directory must be a Git repository!'); - }); - - const git = await createGit(fixture.path); - - await test('Fails on no staged files', async () => { - const { stdout, exitCode } = await aicommits([], { reject: false }); - expect(exitCode).toBe(1); - expect(stdout).toMatch('No staged changes found. Make sure to stage your changes with `git add`.'); - }); - - await aicommits([ - 'config', - 'set', - `OPENAI_KEY=${OPENAI_KEY}`, - ]); - - await test('Excludes files', async () => { - await git('add', ['data.json']); - const statusBefore = await git('status', ['--porcelain', '--untracked-files=no']); - expect(statusBefore.stdout).toBe('A data.json'); - - const { stdout, exitCode } = await aicommits(['--exclude', 'data.json'], { reject: false }); - expect(exitCode).toBe(1); - expect(stdout).toMatch('No staged changes found. Make sure to stage your changes with `git add`.'); - }); - - await test('Generates commit message', async () => { - const committing = aicommits(); - committing.stdout!.on('data', (buffer: Buffer) => { - const stdout = buffer.toString(); - if (stdout.match('└')) { - committing.stdin!.write('y'); - committing.stdin!.end(); - } - }); - - await committing; - - const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']); - expect(statusAfter.stdout).toBe(''); - - const { stdout } = await git('log', ['--oneline']); - console.log('Committed with:', stdout); - }); - - await test('Accepts --generate flag, overriding config', async () => { - data.lastName = 'Osame'; - await fixture.writeJson('data.json', data); - - await git('add', ['data.json']); - - const statusBefore = await git('status', ['--porcelain', '--untracked-files=no']); - expect(statusBefore.stdout).toBe('M data.json'); - - await aicommits([ - 'config', - 'set', - 'generate=4', - ]); - - // Generate flag should override generate config - const committing = aicommits(['--generate', '2']); - - committing.stdout!.on('data', function onPrompt(buffer: Buffer) { - const stdout = buffer.toString(); - if (stdout.match('└')) { - const countChoices = stdout.match(/ {2}[●○]/g)?.length ?? 0; - - // 2 choices should be generated - expect(countChoices).toBe(2); - - committing.stdin!.write('\r'); - committing.stdin!.end(); - committing.stdout?.off('data', onPrompt); - } - }); - - await committing; - - const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']); - expect(statusAfter.stdout).toBe(''); - - const { stdout } = await git('log', ['--oneline']); - console.log('Committed with:', stdout); - - await aicommits([ - 'config', - 'set', - 'generate=1', - ]); - }); - - await test('Generates Japanese commit message via locale config', async () => { - // https://stackoverflow.com/a/15034560/911407 - const japanesePattern = /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFF9F\u4E00-\u9FAF\u3400-\u4DBF]/; - - data.username = 'privatenumber'; - await fixture.writeJson('data.json', data); - - await git('add', ['data.json']); - - const statusBefore = await git('status', ['--porcelain', '--untracked-files=no']); - expect(statusBefore.stdout).toBe('M data.json'); - - await aicommits([ - 'config', - 'set', - 'locale=ja', - ]); - - // Generate flag should override generate config - const committing = aicommits(['--generate', '1']); - - committing.stdout!.on('data', (buffer: Buffer) => { - const stdout = buffer.toString(); - if (stdout.match('└')) { - committing.stdin!.write('y'); - committing.stdin!.end(); - } - }); - - await committing; - - const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']); - expect(statusAfter.stdout).toBe(''); - - const { stdout } = await git('log', ['--oneline']); - console.log('Committed with:', stdout); - - expect(stdout).toMatch(japanesePattern); - }); - - await fixture.rm(); - }); -}); diff --git a/tests/specs/cli/commits.ts b/tests/specs/cli/commits.ts new file mode 100644 index 00000000..717d7169 --- /dev/null +++ b/tests/specs/cli/commits.ts @@ -0,0 +1,137 @@ +import { testSuite, expect } from 'manten'; +import { createFixture, createGit } from '../../utils.js'; + +const { OPENAI_KEY } = process.env; + +export default testSuite(({ describe }) => { + if (process.platform === 'win32') { + // https://github.com/nodejs/node/issues/31409 + console.warn('Skipping tests on Windows because Node.js spawn cant open TTYs'); + return; + } + + if (!OPENAI_KEY) { + console.warn('⚠️ process.env.OPENAI_KEY is necessary to run these tests. Skipping...'); + return; + } + + describe('CLI', async ({ test }) => { + const files = { + '.aicommits': `OPENAI_KEY=${OPENAI_KEY}`, + 'data.json': 'Lorem ipsum dolor sit amet '.repeat(10), + } as const; + + test('Excludes files', async () => { + const { fixture, aicommits } = await createFixture(files); + const git = await createGit(fixture.path); + + await git('add', ['data.json']); + const statusBefore = await git('status', ['--porcelain', '--untracked-files=no']); + expect(statusBefore.stdout).toBe('A data.json'); + + const { stdout, exitCode } = await aicommits(['--exclude', 'data.json'], { reject: false }); + expect(exitCode).toBe(1); + expect(stdout).toMatch('No staged changes found. Make sure to stage your changes with `git add`.'); + await fixture.rm(); + }); + + test('Generates commit message', async () => { + const { fixture, aicommits } = await createFixture(files); + const git = await createGit(fixture.path); + + await git('add', ['data.json']); + + const committing = aicommits(); + committing.stdout!.on('data', (buffer: Buffer) => { + const stdout = buffer.toString(); + if (stdout.match('└')) { + committing.stdin!.write('y'); + committing.stdin!.end(); + } + }); + + await committing; + + const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']); + expect(statusAfter.stdout).toBe(''); + + const { stdout: commitMessage } = await git('log', ['--oneline']); + console.log('Committed with:', commitMessage); + + await fixture.rm(); + }); + + test('Accepts --generate flag, overriding config', async ({ onTestFail }) => { + const { fixture, aicommits } = await createFixture({ + ...files, + '.aicommits': `${files['.aicommits']}\ngenerate=4`, + }); + const git = await createGit(fixture.path); + + await git('add', ['data.json']); + + // Generate flag should override generate config + const committing = aicommits([ + '--generate', '2', + ]); + + // Hit enter to accept the commit message + committing.stdout!.on('data', function onPrompt(buffer: Buffer) { + const stdout = buffer.toString(); + if (stdout.match('└')) { + committing.stdin!.write('\r'); + committing.stdin!.end(); + committing.stdout?.off('data', onPrompt); + } + }); + + const { stdout } = await committing; + const countChoices = stdout.match(/ {2}[●○]/g)?.length ?? 0; + + onTestFail(() => console.log({ stdout })); + expect(countChoices).toBe(2); + + const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']); + expect(statusAfter.stdout).toBe(''); + + const { stdout: commitMessage } = await git('log', ['--oneline']); + console.log('Committed with:', commitMessage); + + await fixture.rm(); + }); + + test('Generates Japanese commit message via locale config', async () => { + // https://stackoverflow.com/a/15034560/911407 + const japanesePattern = /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFF9F\u4E00-\u9FAF\u3400-\u4DBF]/; + + const { fixture, aicommits } = await createFixture({ + ...files, + '.aicommits': `${files['.aicommits']}\nlocale=ja`, + }); + const git = await createGit(fixture.path); + + await git('add', ['data.json']); + + const committing = aicommits(); + + committing.stdout!.on('data', (buffer: Buffer) => { + const stdout = buffer.toString(); + if (stdout.match('└')) { + committing.stdin!.write('y'); + committing.stdin!.end(); + } + }); + + await committing; + + const statusAfter = await git('status', ['--porcelain', '--untracked-files=no']); + expect(statusAfter.stdout).toBe(''); + + const { stdout: commitMessage } = await git('log', ['--oneline']); + console.log('Committed with:', commitMessage); + expect(commitMessage).toMatch(japanesePattern); + + await fixture.rm(); + }); + }); +}); diff --git a/tests/specs/cli/error-cases.ts b/tests/specs/cli/error-cases.ts new file mode 100644 index 00000000..e99602cf --- /dev/null +++ b/tests/specs/cli/error-cases.ts @@ -0,0 +1,24 @@ +import { testSuite, expect } from 'manten'; +import { createFixture, createGit } from '../../utils.js'; + +export default testSuite(({ describe }) => { + describe('Error cases', async ({ test }) => { + test('Fails on non-Git project', async () => { + const { fixture, aicommits } = await createFixture(); + const { stdout, exitCode } = await aicommits([], { reject: false }); + expect(exitCode).toBe(1); + expect(stdout).toMatch('The current directory must be a Git repository!'); + await fixture.rm(); + }); + + test('Fails on no staged files', async () => { + const { fixture, aicommits } = await createFixture(); + await createGit(fixture.path); + + const { stdout, exitCode } = await aicommits([], { reject: false }); + expect(exitCode).toBe(1); + expect(stdout).toMatch('No staged changes found. Make sure to stage your changes with `git add`.'); + await fixture.rm(); + }); + }); +}); diff --git a/tests/specs/cli/index.ts b/tests/specs/cli/index.ts new file mode 100644 index 00000000..85d3079c --- /dev/null +++ b/tests/specs/cli/index.ts @@ -0,0 +1,8 @@ +import { testSuite } from 'manten'; + +export default testSuite(({ describe }) => { + describe('CLI', ({ runTestSuite }) => { + runTestSuite(import('./error-cases.js')); + runTestSuite(import('./commits.js')); + }); +}); diff --git a/tests/specs/config.ts b/tests/specs/config.ts index 1e04c03b..d78b6c46 100644 --- a/tests/specs/config.ts +++ b/tests/specs/config.ts @@ -1,15 +1,11 @@ import fs from 'fs/promises'; import path from 'path'; import { testSuite, expect } from 'manten'; -import { createFixture } from 'fs-fixture'; -import { createAicommits } from '../utils.js'; +import { createFixture } from '../utils.js'; export default testSuite(({ describe }) => { describe('config', async ({ test }) => { - const fixture = await createFixture(); - const aicommits = createAicommits({ - home: fixture.path, - }); + const { fixture, aicommits } = await createFixture(); const configPath = path.join(fixture.path, '.aicommits'); const openAiToken = 'OPENAI_KEY=sk-abc'; diff --git a/tests/utils.ts b/tests/utils.ts index 3e479253..46b580e5 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,18 +1,17 @@ import path from 'path'; import { execa, execaNode, type Options } from 'execa'; +import { + createFixture as createFixtureBase, + type FileTree, + type FsFixture, +} from 'fs-fixture'; const aicommitsPath = path.resolve('./dist/cli.mjs'); -export const createAicommits = ({ - cwd, - home, -}: { - cwd?: string; - home: string; -}) => { +const createAicommits = (fixture: FsFixture) => { const homeEnv = { - HOME: home, // Linux - USERPROFILE: home, // Windows + HOME: fixture.path, // Linux + USERPROFILE: fixture.path, // Windows }; return ( @@ -20,7 +19,7 @@ export const createAicommits = ({ options?: Options, ) => execaNode(aicommitsPath, args, { ...options, - cwd, + cwd: fixture.path, extendEnv: false, env: { ...homeEnv, @@ -61,3 +60,15 @@ export const createGit = async (cwd: string) => { return git; }; + +export const createFixture = async ( + source?: string | FileTree, +) => { + const fixture = await createFixtureBase(source); + const aicommits = createAicommits(fixture); + + return { + fixture, + aicommits, + }; +};