diff --git a/src/utils/create-app-task-run-init-script.ts b/src/utils/create-app-task-run-init-script.ts index 336ddaf9..a13b2161 100644 --- a/src/utils/create-app-task-run-init-script.ts +++ b/src/utils/create-app-task-run-init-script.ts @@ -4,7 +4,7 @@ import { ensureTargetPath } from './ensure-target-path' import { GetArgsResult } from './get-args-result' import { deleteInitScript, getInitScript, InitScript } from './get-init-script' import { getPackageJson } from './get-package-json' -import { initCheckVersion } from './init-check-version' +import { initScriptVersion } from './init-script-version' import { searchAndReplace } from './search-and-replace' import { Task, taskFail } from './vendor/clack-tasks' import { namesValues } from './vendor/names' @@ -23,7 +23,7 @@ export function createAppTaskRunInitScript(args: GetArgsResult): Task { log.warn(`Running init script`) } - await initCheckVersion(init) + await initScriptVersion(init.versions, args.verbose) if (args.verbose) { log.warn(`initCheckVersion done`) } diff --git a/src/utils/get-init-script.ts b/src/utils/get-init-script.ts index 88f97b8c..96d070f8 100644 --- a/src/utils/get-init-script.ts +++ b/src/utils/get-init-script.ts @@ -26,6 +26,12 @@ export function deleteInitScript(targetDirectory: string) { writeFileSync(path, JSON.stringify(contents, undefined, 2) + '\n') } +const InitScriptVersionsSchema = z.object({ + adb: z.string().optional(), + anchor: z.string().optional(), + solana: z.string().optional(), +}) + const InitScriptSchema = z .object({ instructions: z.array(z.string()).optional(), @@ -37,14 +43,9 @@ const InitScriptSchema = z }), ) .optional(), - versions: z - .object({ - adb: z.string().optional(), - anchor: z.string().optional(), - solana: z.string().optional(), - }) - .optional(), + versions: InitScriptVersionsSchema.optional(), }) .optional() export type InitScript = z.infer +export type InitScriptVersions = z.infer diff --git a/src/utils/init-check-version.ts b/src/utils/init-check-version.ts deleted file mode 100644 index 1d41c7fc..00000000 --- a/src/utils/init-check-version.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { InitScript } from './get-init-script' -import { initCheckVersionAdb } from './init-check-version-adb' -import { initCheckVersionAnchor } from './init-check-version-anchor' -import { initCheckVersionSolana } from './init-check-version-solana' - -export async function initCheckVersion(init: InitScript) { - if (init?.versions?.adb) { - await initCheckVersionAdb(init.versions.adb) - } - if (init?.versions?.anchor) { - await initCheckVersionAnchor(init.versions.anchor) - } - if (init?.versions?.solana) { - await initCheckVersionSolana(init.versions.solana) - } -} diff --git a/src/utils/init-check-version-adb.ts b/src/utils/init-script-version-adb.ts similarity index 78% rename from src/utils/init-check-version-adb.ts rename to src/utils/init-script-version-adb.ts index c95ae4db..446b49bc 100644 --- a/src/utils/init-check-version-adb.ts +++ b/src/utils/init-script-version-adb.ts @@ -3,9 +3,15 @@ import { bold, yellow } from 'picocolors' import { getVersion } from './get-version' import { validateVersion } from './validate-version' -export async function initCheckVersionAdb(required: string) { +export async function initScriptVersionAdb(required?: string, verbose = false) { + if (!required) { + return + } try { const { valid, version } = validateVersion({ required, version: getVersion('adb') }) + if (verbose) { + log.warn(`initScriptVersionAdb: required: ${required}, version: ${version ?? '*none*'}, valid: ${valid}`) + } if (!version) { log.warn( [ diff --git a/src/utils/init-check-version-anchor.ts b/src/utils/init-script-version-anchor.ts similarity index 75% rename from src/utils/init-check-version-anchor.ts rename to src/utils/init-script-version-anchor.ts index 1aceef25..daae2546 100644 --- a/src/utils/init-check-version-anchor.ts +++ b/src/utils/init-script-version-anchor.ts @@ -3,9 +3,15 @@ import { bold, yellow } from 'picocolors' import { getVersion } from './get-version' import { validateVersion } from './validate-version' -export async function initCheckVersionAnchor(required: string) { +export async function initScriptVersionAnchor(required?: string, verbose = false) { + if (!required) { + return + } try { const { valid, version } = validateVersion({ required, version: getVersion('anchor') }) + if (verbose) { + log.warn(`initScriptVersionAnchor: required: ${required}, version: ${version ?? '*none*'}, valid: ${valid}`) + } if (!version) { log.warn( [ diff --git a/src/utils/init-check-version-solana.ts b/src/utils/init-script-version-solana.ts similarity index 75% rename from src/utils/init-check-version-solana.ts rename to src/utils/init-script-version-solana.ts index e60f1f55..b3a55563 100644 --- a/src/utils/init-check-version-solana.ts +++ b/src/utils/init-script-version-solana.ts @@ -3,9 +3,15 @@ import { bold, yellow } from 'picocolors' import { getVersion } from './get-version' import { validateVersion } from './validate-version' -export async function initCheckVersionSolana(required: string) { +export async function initScriptVersionSolana(required?: string, verbose = false) { + if (!required) { + return + } try { const { valid, version } = validateVersion({ required, version: getVersion('solana') }) + if (verbose) { + log.warn(`initScriptVersionSolana: required: ${required}, version: ${version ?? '*none*'}, valid: ${valid}`) + } if (!version) { log.warn( [ diff --git a/src/utils/init-script-version.ts b/src/utils/init-script-version.ts new file mode 100644 index 00000000..4837d6e2 --- /dev/null +++ b/src/utils/init-script-version.ts @@ -0,0 +1,17 @@ +import { log } from '@clack/prompts' +import { InitScriptVersions } from './get-init-script' +import { initScriptVersionAdb } from './init-script-version-adb' +import { initScriptVersionAnchor } from './init-script-version-anchor' +import { initScriptVersionSolana } from './init-script-version-solana' + +export async function initScriptVersion(versions?: InitScriptVersions, verbose = false) { + if (!versions) { + if (verbose) { + log.warn(`initScriptCheckVersion: no versions found`) + } + return + } + await initScriptVersionAdb(versions.adb, verbose) + await initScriptVersionAnchor(versions.anchor, verbose) + await initScriptVersionSolana(versions.solana, verbose) +} diff --git a/test/get-version.test.ts b/test/get-version.test.ts new file mode 100644 index 00000000..5cd7010d --- /dev/null +++ b/test/get-version.test.ts @@ -0,0 +1,44 @@ +import * as childProcess from 'node:child_process' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getVersion, versionCommands } from '../src/utils/get-version' + +vi.mock('node:child_process', () => ({ + execSync: vi.fn(), +})) + +describe('versionCommands', () => { + it('should have the expected commands', () => { + expect(Object.keys(versionCommands)).toEqual(['adb', 'anchor', 'avm', 'rust', 'solana']) + }) + + it('should have correct structure for each command', () => { + for (const cmd of Object.values(versionCommands)) { + expect(cmd).toHaveProperty('command', expect.any(String)) + expect(cmd).toHaveProperty('name', expect.any(String)) + expect(cmd).toHaveProperty('regex', expect.any(RegExp)) + } + }) +}) + +describe('getVersion', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + it('should return version for known command', () => { + ;(childProcess.execSync as any).mockReturnValue('anchor-cli 0.24.2\n') + const version = getVersion('anchor') + expect(version).toBe('0.24.2') + expect(childProcess.execSync).toHaveBeenCalledWith('anchor --version', { stdio: ['ignore', 'pipe', 'ignore'] }) + }) + + it('should throw error for unknown command', () => { + expect(() => getVersion('unknown' as any)).toThrow('Unknown command unknown') + }) + + it('should return undefined if parsing fails', () => { + ;(childProcess.execSync as any).mockReturnValue('Invalid output\n') + const version = getVersion('anchor') + expect(version).toBeUndefined() + }) +}) diff --git a/test/init-script-version-adb.test.ts b/test/init-script-version-adb.test.ts new file mode 100644 index 00000000..34d89b29 --- /dev/null +++ b/test/init-script-version-adb.test.ts @@ -0,0 +1,87 @@ +import { log } from '@clack/prompts' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getVersion } from '../src/utils/get-version' +import { initScriptVersionAdb } from '../src/utils/init-script-version-adb' +import { validateVersion } from '../src/utils/validate-version' + +vi.mock('../src/utils/get-version') +vi.mock('../src/utils/validate-version') +vi.mock('@clack/prompts', () => ({ + log: { + warn: vi.fn(), + }, +})) + +describe('initScriptVersionAdb', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should return early if no required version is provided', async () => { + await initScriptVersionAdb() + expect(getVersion).not.toHaveBeenCalled() + expect(validateVersion).not.toHaveBeenCalled() + expect(log.warn).not.toHaveBeenCalled() + }) + + it('should log warning if adb version is not found', async () => { + const required = '1.0.0' + vi.mocked(getVersion).mockReturnValue(undefined) + vi.mocked(validateVersion).mockReturnValue({ valid: false, version: undefined }) + await initScriptVersionAdb(required) + expect(getVersion).toHaveBeenCalledWith('adb') + expect(validateVersion).toHaveBeenCalledWith({ required, version: undefined }) + expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('Could not find adb version. Please install adb.')) + }) + + it('should log warning if adb version does not satisfy the requirement', async () => { + const required = '1.0.0' + const version = '0.9.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: false, version }) + await initScriptVersionAdb(required) + expect(getVersion).toHaveBeenCalledWith('adb') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).toHaveBeenCalledWith( + expect.stringContaining(`Found adb version ${version}. Expected adb version ${required}.`), + ) + }) + + it('should not log warning if adb version satisfies the requirement', async () => { + const required = '1.0.0' + const version = '1.0.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: true, version }) + await initScriptVersionAdb(required) + expect(getVersion).toHaveBeenCalledWith('adb') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).not.toHaveBeenCalled() + }) + + it('should log verbose message if verbose is true', async () => { + const required = '1.0.0' + const version = '1.0.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: true, version }) + await initScriptVersionAdb(required, true) + expect(getVersion).toHaveBeenCalledWith('adb') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).toHaveBeenCalledWith( + `initScriptVersionAdb: required: ${required}, version: ${version}, valid: true`, + ) + }) + + it('should log error if an exception occurs', async () => { + const required = '1.0.0' + const error = new Error('Test error') + vi.mocked(getVersion).mockImplementation(() => { + throw error + }) + await initScriptVersionAdb(required) + expect(log.warn).toHaveBeenCalledWith(`Error ${error}`) + }) +}) diff --git a/test/init-script-version-anchor.test.ts b/test/init-script-version-anchor.test.ts new file mode 100644 index 00000000..de459298 --- /dev/null +++ b/test/init-script-version-anchor.test.ts @@ -0,0 +1,89 @@ +import { log } from '@clack/prompts' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getVersion } from '../src/utils/get-version' +import { initScriptVersionAnchor } from '../src/utils/init-script-version-anchor' +import { validateVersion } from '../src/utils/validate-version' + +vi.mock('../src/utils/get-version') +vi.mock('../src/utils/validate-version') +vi.mock('@clack/prompts', () => ({ + log: { + warn: vi.fn(), + }, +})) + +describe('initScriptVersionAnchor', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should return early if no required version is provided', async () => { + await initScriptVersionAnchor() + expect(getVersion).not.toHaveBeenCalled() + expect(validateVersion).not.toHaveBeenCalled() + expect(log.warn).not.toHaveBeenCalled() + }) + + it('should log warning if anchor version is not found', async () => { + const required = '1.0.0' + vi.mocked(getVersion).mockReturnValue(undefined) + vi.mocked(validateVersion).mockReturnValue({ valid: false, version: undefined }) + await initScriptVersionAnchor(required) + expect(getVersion).toHaveBeenCalledWith('anchor') + expect(validateVersion).toHaveBeenCalledWith({ required, version: undefined }) + expect(log.warn).toHaveBeenCalledWith( + expect.stringContaining('Could not find Anchor version. Please install Anchor.'), + ) + }) + + it('should log warning if anchor version does not satisfy the requirement', async () => { + const required = '1.0.0' + const version = '0.9.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: false, version }) + await initScriptVersionAnchor(required) + expect(getVersion).toHaveBeenCalledWith('anchor') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).toHaveBeenCalledWith( + expect.stringContaining(`Found Anchor version ${version}. Expected Anchor version ${required}.`), + ) + }) + + it('should not log warning if anchor version satisfies the requirement', async () => { + const required = '1.0.0' + const version = '1.0.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: true, version }) + await initScriptVersionAnchor(required) + expect(getVersion).toHaveBeenCalledWith('anchor') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).not.toHaveBeenCalled() + }) + + it('should log verbose message if verbose is true', async () => { + const required = '1.0.0' + const version = '1.0.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: true, version }) + await initScriptVersionAnchor(required, true) + expect(getVersion).toHaveBeenCalledWith('anchor') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).toHaveBeenCalledWith( + `initScriptVersionAnchor: required: ${required}, version: ${version}, valid: true`, + ) + }) + + it('should log error if an exception occurs', async () => { + const required = '1.0.0' + const error = new Error('Test error') + vi.mocked(getVersion).mockImplementation(() => { + throw error + }) + await initScriptVersionAnchor(required) + expect(log.warn).toHaveBeenCalledWith(`Error ${error}`) + }) +}) diff --git a/test/init-script-version-solana.test.ts b/test/init-script-version-solana.test.ts new file mode 100644 index 00000000..11a9217a --- /dev/null +++ b/test/init-script-version-solana.test.ts @@ -0,0 +1,89 @@ +import { log } from '@clack/prompts' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getVersion } from '../src/utils/get-version' +import { initScriptVersionSolana } from '../src/utils/init-script-version-solana' +import { validateVersion } from '../src/utils/validate-version' + +vi.mock('../src/utils/get-version') +vi.mock('../src/utils/validate-version') +vi.mock('@clack/prompts', () => ({ + log: { + warn: vi.fn(), + }, +})) + +describe('initScriptVersionSolana', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should return early if no required version is provided', async () => { + await initScriptVersionSolana() + expect(getVersion).not.toHaveBeenCalled() + expect(validateVersion).not.toHaveBeenCalled() + expect(log.warn).not.toHaveBeenCalled() + }) + + it('should log warning if solana version is not found', async () => { + const required = '1.0.0' + vi.mocked(getVersion).mockReturnValue(undefined) + vi.mocked(validateVersion).mockReturnValue({ valid: false, version: undefined }) + await initScriptVersionSolana(required) + expect(getVersion).toHaveBeenCalledWith('solana') + expect(validateVersion).toHaveBeenCalledWith({ required, version: undefined }) + expect(log.warn).toHaveBeenCalledWith( + expect.stringContaining('Could not find Solana version. Please install Solana.'), + ) + }) + + it('should log warning if solana version does not satisfy the requirement', async () => { + const required = '1.0.0' + const version = '0.9.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: false, version }) + await initScriptVersionSolana(required) + expect(getVersion).toHaveBeenCalledWith('solana') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).toHaveBeenCalledWith( + expect.stringContaining(`Found Solana version ${version}. Expected Solana version ${required}.`), + ) + }) + + it('should not log warning if solana version satisfies the requirement', async () => { + const required = '1.0.0' + const version = '1.0.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: true, version }) + await initScriptVersionSolana(required) + expect(getVersion).toHaveBeenCalledWith('solana') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).not.toHaveBeenCalled() + }) + + it('should log verbose message if verbose is true', async () => { + const required = '1.0.0' + const version = '1.0.0' + vi.mocked(getVersion).mockReturnValue(version) + vi.mocked(validateVersion).mockReturnValue({ valid: true, version }) + await initScriptVersionSolana(required, true) + expect(getVersion).toHaveBeenCalledWith('solana') + expect(validateVersion).toHaveBeenCalledWith({ required, version }) + expect(log.warn).toHaveBeenCalledWith( + `initScriptVersionSolana: required: ${required}, version: ${version}, valid: true`, + ) + }) + + it('should log error if an exception occurs', async () => { + const required = '1.0.0' + const error = new Error('Test error') + vi.mocked(getVersion).mockImplementation(() => { + throw error + }) + await initScriptVersionSolana(required) + expect(log.warn).toHaveBeenCalledWith(`Error ${error}`) + }) +}) diff --git a/test/parse-version.test.ts b/test/parse-version.test.ts new file mode 100644 index 00000000..882240ef --- /dev/null +++ b/test/parse-version.test.ts @@ -0,0 +1,27 @@ +import * as childProcess from 'node:child_process' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { parseVersion } from '../src/utils/parse-version' + +vi.mock('node:child_process', () => ({ + execSync: vi.fn(), +})) + +describe('parseVersion', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + it('should parse version correctly', () => { + ;(childProcess.execSync as any).mockReturnValue('Version 1.2.3\n') + const version = parseVersion('some command', /Version (\d+\.\d+\.\d+)/) + expect(version).toBe('1.2.3') + expect(childProcess.execSync).toHaveBeenCalledWith('some command', { stdio: ['ignore', 'pipe', 'ignore'] }) + }) + + it('should throw error if regex does not match', () => { + ;(childProcess.execSync as any).mockReturnValue('Invalid output\n') + expect(() => parseVersion('some command', /Version (\d+\.\d+\.\d+)/)).toThrow( + 'Unable to parse version: Invalid output', + ) + }) +}) diff --git a/test/validate-version.test.ts b/test/validate-version.test.ts new file mode 100644 index 00000000..af9a6e01 --- /dev/null +++ b/test/validate-version.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { validateVersion } from '../src/utils/validate-version' + +describe('validateVersion', () => { + it('should return valid true when version equals required', () => { + const result = validateVersion({ required: '1.0.0', version: '1.0.0' }) + expect(result).toEqual({ valid: true, version: '1.0.0' }) + }) + + it('should return valid true when version is greater than required', () => { + const result = validateVersion({ required: '1.0.0', version: '1.1.0' }) + expect(result).toEqual({ valid: true, version: '1.1.0' }) + }) + + it('should return valid false when version is less than required', () => { + const result = validateVersion({ required: '1.0.0', version: '0.9.0' }) + expect(result).toEqual({ valid: false, version: '0.9.0' }) + }) + + it('should return valid false when version is undefined', () => { + const result = validateVersion({ required: '1.0.0', version: undefined }) + expect(result).toEqual({ valid: false, version: undefined }) + }) + + it('should return valid false when version is invalid', () => { + const result = validateVersion({ required: '1.0.0', version: 'invalid' }) + expect(result).toEqual({ valid: false, version: 'invalid' }) + }) +})