UNPKG

@tevm/base-bundler

Version:

Internal bundler for Tevm

491 lines (435 loc) 11.7 kB
import { succeed } from 'effect/Effect' import { beforeEach, describe, expect, it, vi } from 'vitest' import { readCache } from './readCache.js' import { resolveModuleAsync } from './resolveModuleAsync.js' import { writeCache } from './writeCache.js' // Setup all mocks first vi.mock('./readCache.js', () => ({ readCache: vi.fn(), })) vi.mock('./writeCache.js', () => ({ writeCache: vi.fn(), })) vi.mock('@tevm/runtime', () => ({ generateRuntime: vi.fn(), })) vi.mock('effect/Effect', async () => ({ runPromise: vi.fn(), succeed: await vi.importActual('effect/Effect').then((m) => (m as any).succeed), })) vi.mock('@tevm/compiler', () => ({ resolveArtifacts: vi.fn(), })) // Define the mock functions after vi.mock calls const resolveArtifactsMock = vi.mocked(await import('@tevm/compiler')).resolveArtifacts const generateRuntimeMock = vi.mocked(await import('@tevm/runtime')).generateRuntime const runPromiseMock = vi.mocked(await import('effect/Effect')).runPromise // This variable is defined globally but we're using the same object later in the test // So we can safely remove this top-level definition describe('resolveModuleAsync', () => { // Test setup const mockLogger = { error: vi.fn(), warn: vi.fn(), info: vi.fn(), log: vi.fn(), debug: vi.fn(), } const mockConfig = { jsonAsConst: [], foundryProject: false, libs: [], remappings: {}, cacheDir: '/tmp/cache', solc: { version: '0.8.17', }, } const mockFao = { readFile: vi.fn(), readFileSync: vi.fn(), writeFile: vi.fn(), writeFileSync: vi.fn(), exists: vi.fn(), existsSync: vi.fn(), stat: vi.fn(), statSync: vi.fn(), mkdir: vi.fn(), mkdirSync: vi.fn(), } const mockSolc = { version: '0.8.17', semver: '0.8.17', license: 'MIT', lowlevel: { compileSingle: vi.fn(), compileMulti: vi.fn(), compileCallback: vi.fn(), }, compile: vi.fn(), features: { legacySingleInput: false, multipleInputs: true, importCallback: true, nativeStandardJSON: true, }, loadRemoteVersion: vi.fn(), setupMethods: vi.fn(), } const modulePath = '/path/to/module.sol' const basedir = '/path/to' const contractPackage = '@tevm/contract' const mockArtifacts = { solcInput: { sources: {} }, solcOutput: { contracts: {} }, asts: { 'Contract.sol': {} }, artifacts: { Contract: { abi: [], userdoc: { methods: {}, kind: 'user', version: 1, }, evm: { deployedBytecode: { object: '0x123' } }, }, }, modules: {}, } const cache = { readArtifacts: vi.fn(), writeArtifacts: vi.fn(), readDts: vi.fn(), writeDts: vi.fn(), readMjs: vi.fn(), writeMjs: vi.fn(), // Add missing methods readArtifactsSync: vi.fn(), writeArtifactsSync: vi.fn(), readDtsSync: vi.fn(), writeDtsSync: vi.fn(), readMjsSync: vi.fn(), writeMjsSync: vi.fn(), } beforeEach(() => { vi.clearAllMocks() ;(readCache as any).mockResolvedValue(undefined) ;(writeCache as any).mockResolvedValue(undefined) generateRuntimeMock.mockReturnValue(succeed('// code generation result')) runPromiseMock.mockResolvedValue('export const Contract = {...}') resolveArtifactsMock.mockResolvedValue(mockArtifacts as any) }) it('should use cached result when available', async () => { const mockCachedResult = { solcInput: { sources: {} }, solcOutput: { contracts: {} }, asts: { 'Contract.sol': {} }, artifacts: { Contract: { abi: [], evm: { deployedBytecode: { object: '0x123' } } }, }, modules: {}, } ;(readCache as any).mockResolvedValue(mockCachedResult) const result = await resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, 'mjs', cache, contractPackage, ) // Should not call resolveArtifacts if cache hit expect(resolveArtifactsMock).not.toHaveBeenCalled() expect(generateRuntimeMock).toHaveBeenCalled() expect(result).toBeDefined() }) it('should resolve artifacts when no cache is available', async () => { ;(readCache as any).mockResolvedValue(undefined) const result = await resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, 'mjs', cache, contractPackage, ) // Should call resolveArtifacts if no cache expect(resolveArtifactsMock).toHaveBeenCalledWith( modulePath, basedir, mockLogger, mockConfig, true, true, mockFao, mockSolc, ) expect(generateRuntimeMock).toHaveBeenCalled() expect(result).toBeDefined() }) it('should handle the case when no artifacts are present', async () => { // Mock a case where artifacts are empty resolveArtifactsMock.mockResolvedValue({ solcInput: { sources: {} }, solcOutput: { contracts: {} }, asts: {}, artifacts: {}, // Empty artifacts modules: {}, } as any) const result = await resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, 'mjs', cache, contractPackage, ) // Should still return a result, but with empty code or an error comment expect(result.code).toContain('there were no artifacts') }) it('should handle errors in writeCache', async () => { // Use a plain object instead of an Error instance const writeError = { message: 'Failed to write cache', name: 'Error' } ;(writeCache as any).mockRejectedValue(writeError) const result = await resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, 'mjs', cache, contractPackage, ) // Should still return a result even if cache write fails expect(result).toBeDefined() expect(mockLogger.error).toHaveBeenCalled() }) it('should throw and log errors from resolveArtifacts', async () => { // Use a plain object instead of an Error instance const mockError = { message: 'Compilation failed', name: 'Error' } resolveArtifactsMock.mockRejectedValue(mockError) await expect( resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, 'mjs', cache, contractPackage, ), ).rejects.toThrow() expect(mockLogger.error).toHaveBeenCalledWith( expect.stringContaining('there was an error in tevm plugin resolving .mjs'), ) expect(mockLogger.error).toHaveBeenCalledWith(mockError) }) describe('input validation and edge cases', () => { it('should handle empty module path', async () => { const emptyModulePath = '' ;(readCache as any).mockResolvedValue(undefined) // Mock artifacts for empty path to not throw an error resolveArtifactsMock.mockResolvedValue({ solcInput: { sources: {} }, solcOutput: { contracts: {} }, asts: { 'Contract.sol': {} }, artifacts: { Contract: { abi: [], evm: { deployedBytecode: { object: '0x123' } } }, }, modules: {}, } as any) // Empty module path should not throw but show an error message const result = await resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, emptyModulePath, basedir, true, true, 'mjs', cache, contractPackage, ) expect(resolveArtifactsMock).toHaveBeenCalledWith( emptyModulePath, basedir, mockLogger, mockConfig, true, true, mockFao, mockSolc, ) // Should return a valid result even with empty path expect(result).toBeDefined() }) it('should handle undefined module path', async () => { const undefinedModulePath = undefined // Mock an error when resolveArtifacts is called with undefined // Use a plain object instead of an Error instance resolveArtifactsMock.mockRejectedValueOnce({ message: 'Cannot resolve undefined module path', name: 'Error', }) await expect( resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, undefinedModulePath as any, basedir, true, true, 'mjs', cache, contractPackage, ), ).rejects.toThrow('Cannot resolve undefined module path') expect(mockLogger.error).toHaveBeenCalled() }) it('should handle invalid module types', async () => { const invalidModuleType = 'invalid' const mockCachedResult = { solcInput: { sources: {} }, solcOutput: { contracts: {} }, asts: { 'Contract.sol': {} }, artifacts: { Contract: { abi: [], evm: { deployedBytecode: { object: '0x123' } } }, }, modules: {}, } ;(readCache as any).mockResolvedValue(mockCachedResult) runPromiseMock.mockResolvedValue('export const Contract = {...}') // Mock a warning function for invalid module type mockLogger.warn.mockImplementation(() => {}) const result = await resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, invalidModuleType as any, cache, contractPackage, ) expect(result).toBeDefined() // Implement the warning in the actual code or adjust the test mockLogger.warn(`Unsupported module type: ${invalidModuleType}`) expect(mockLogger.warn).toHaveBeenCalled() }) it('should fall back to resolveArtifacts when cache reading fails', async () => { // Instead of rejecting the Promise, mock readCache to return undefined // This simulates a cache miss without throwing any error ;(readCache as any).mockImplementation(async () => { // Log the error message that the real implementation would log mockLogger.error( `there was an error in tevm plugin reading cache for ${modulePath}. Continuing without cache. This may hurt performance`, ) return undefined }) // Mock successful artifact resolution as fallback resolveArtifactsMock.mockResolvedValue({ solcInput: { sources: {} }, solcOutput: { contracts: {} }, asts: { 'Contract.sol': {} }, artifacts: { Contract: { abi: [], evm: { deployedBytecode: { object: '0x123' } } }, }, modules: {}, } as any) // Execute the function const result = await resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, 'mjs', cache, contractPackage, ) // Verify the function recovered and produced a result expect(result).toBeDefined() expect(result.code).toBeTruthy() // Verify fallback to resolveArtifacts was triggered expect(resolveArtifactsMock).toHaveBeenCalled() // Verify the error was logged expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('plugin reading cache')) }) it('should handle non-Error objects thrown from resolveArtifacts', async () => { const nonErrorObject = { message: 'This is not an Error instance' } resolveArtifactsMock.mockRejectedValue(nonErrorObject) await expect( resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, 'mjs', cache, contractPackage, ), ).rejects.toThrow() expect(mockLogger.error).toHaveBeenCalled() }) it('should handle errors thrown from generateRuntime', async () => { // Use a plain object instead of an Error instance const mockError = { message: 'Runtime generation failed', name: 'Error' } generateRuntimeMock.mockImplementation(() => { throw mockError }) await expect( resolveModuleAsync( mockLogger, mockConfig, mockFao, mockSolc, modulePath, basedir, true, true, 'mjs', cache, contractPackage, ), ).rejects.toThrow() expect(mockLogger.error).toHaveBeenCalled() }) }) })