phper/electron/services/__tests__/GoManager.test.ts
Ethanfly 9614a3d234 feat: add Go version management support
- Add GoManager service for downloading and managing Go versions
- Implement Go version detection and installation
- Add GoManager Vue component with version selection UI
- Update main process to handle Go-related IPC calls
- Add Jest testing configuration and GoManager unit tests
- Update service store to include Go management
- Add routing for Go manager page
- Include Kiro specs and steering documentation
2026-01-13 18:30:26 +08:00

1065 lines
41 KiB
TypeScript

import { join } from 'path'
import { existsSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import * as fc from 'fast-check'
// Minimal ConfigStore interface for testing
interface IConfigStore {
getBasePath(): string
getTempPath(): string
get(key: string): any
set(key: string, value: any): void
}
// Mock ConfigStore for testing
class MockConfigStore implements IConfigStore {
private mockBasePath: string
private config: Record<string, any> = {}
constructor() {
// Create a temporary directory for testing
const testDir = join(tmpdir(), 'phper-test-' + Date.now())
mkdirSync(testDir, { recursive: true })
this.mockBasePath = testDir
this.config = {
goVersions: [],
activeGoVersion: ''
}
}
getBasePath(): string {
return this.mockBasePath
}
getTempPath(): string {
return join(this.mockBasePath, 'temp')
}
get(key: string): any {
return this.config[key]
}
set(key: string, value: any): void {
this.config[key] = value
}
cleanup(): void {
if (existsSync(this.mockBasePath)) {
rmSync(this.mockBasePath, { recursive: true, force: true })
}
}
}
// Simplified GoManager for testing (without external dependencies)
class TestGoManager {
private configStore: IConfigStore
constructor(configStore: IConfigStore) {
this.configStore = configStore
}
getGoBasePath(): string {
return join(this.configStore.getBasePath(), 'go')
}
getGoPath(version: string): string {
return join(this.getGoBasePath(), `go-${version}`)
}
getDefaultGoPath(): string {
return join(this.getGoBasePath(), 'workspace')
}
async getInstalledVersions(): Promise<any[]> {
const goBasePath = this.getGoBasePath()
if (!existsSync(goBasePath)) {
return []
}
// In a real implementation, this would scan directories
// For testing, just return empty array
return []
}
async validateInstallation(version: string): Promise<boolean> {
const goPath = this.getGoPath(version)
const goExe = join(goPath, 'bin', 'go.exe')
return existsSync(goExe)
}
// Method to test API response parsing
parseApiResponse(releases: any[]): any[] {
const availableVersions: any[] = []
for (const release of releases) {
if (release.stable) {
const windowsFile = release.files?.find((f: any) =>
f.os === 'windows' && f.arch === 'amd64' && f.kind === 'archive'
)
if (windowsFile) {
availableVersions.push({
version: release.version,
stable: release.stable,
downloadUrl: `https://golang.org/dl/${windowsFile.filename}`,
size: windowsFile.size,
sha256: windowsFile.sha256
})
}
}
}
return availableVersions.slice(0, 20)
}
}
// Simple test runner for Node.js environment
async function runTests() {
console.log('Running GoManager tests...')
// Basic test implementation without external framework
const tests = [
{
name: 'GoManager initialization',
test: () => {
const mockStore = new MockConfigStore()
const manager = new TestGoManager(mockStore)
console.assert(manager instanceof TestGoManager, 'GoManager should initialize correctly')
mockStore.cleanup()
return true
}
},
{
name: 'Path generation',
test: () => {
const mockStore = new MockConfigStore()
const manager = new TestGoManager(mockStore)
const basePath = manager.getGoBasePath()
const versionPath = manager.getGoPath('go1.21.5')
const goPath = manager.getDefaultGoPath()
console.assert(basePath.includes('go'), 'Base path should contain go directory')
console.assert(versionPath.includes('go-go1.21.5'), 'Version path should contain version')
console.assert(goPath.includes('workspace'), 'GOPATH should contain workspace')
mockStore.cleanup()
return true
}
},
{
name: 'Empty versions list',
test: async () => {
const mockStore = new MockConfigStore()
const manager = new TestGoManager(mockStore)
const versions = await manager.getInstalledVersions()
console.assert(Array.isArray(versions), 'Should return array')
console.assert(versions.length === 0, 'Should return empty array for no installations')
mockStore.cleanup()
return true
}
},
{
name: 'Configuration integration',
test: () => {
const mockStore = new MockConfigStore()
const manager = new TestGoManager(mockStore)
// Test config store integration
const activeVersion = mockStore.get('activeGoVersion')
const goVersions = mockStore.get('goVersions')
console.assert(typeof activeVersion === 'string', 'Active version should be string')
console.assert(Array.isArray(goVersions), 'Go versions should be array')
mockStore.cleanup()
return true
}
},
{
name: 'Installation validation',
test: async () => {
const mockStore = new MockConfigStore()
const manager = new TestGoManager(mockStore)
// Test validation for non-existent version
const isValid = await manager.validateInstallation('go1.99.99')
console.assert(isValid === false, 'Should return false for non-existent version')
mockStore.cleanup()
return true
}
}
]
let passed = 0
let failed = 0
for (const test of tests) {
try {
const result = await test.test()
if (result) {
console.log(`${test.name}`)
passed++
} else {
console.log(`${test.name}`)
failed++
}
} catch (error) {
console.log(`${test.name}: ${error}`)
failed++
}
}
console.log(`\nTests completed: ${passed} passed, ${failed} failed`)
return failed === 0
}
// Run tests if this file is executed directly
if (require.main === module) {
runTests().then(success => {
process.exit(success ? 0 : 1)
}).catch(error => {
console.error('Test execution failed:', error)
process.exit(1)
})
}
// Export for potential use in other test files
export { TestGoManager, MockConfigStore }
// Property-based tests using Jest and fast-check
describe('Go Version Management Properties', () => {
test('Property 1: API Version Fetching Consistency', () => {
// **Feature: go-version-management, Property 1: API Version Fetching Consistency**
fc.assert(fc.property(
fc.array(fc.record({
version: fc.string({ minLength: 1 }),
stable: fc.boolean(),
files: fc.array(fc.record({
os: fc.constantFrom('windows', 'linux', 'darwin'),
arch: fc.constantFrom('amd64', '386', 'arm64'),
kind: fc.constantFrom('archive', 'installer', 'source'),
filename: fc.string({ minLength: 1 }),
size: fc.nat(),
sha256: fc.string({ minLength: 64, maxLength: 64 }).filter(s => /^[0-9a-f]+$/i.test(s))
}))
})),
(mockApiResponse) => {
const mockStore = new MockConfigStore()
const manager = new TestGoManager(mockStore)
try {
const result = manager.parseApiResponse(mockApiResponse)
// Verify all returned versions have required fields
result.forEach(version => {
expect(version).toHaveProperty('version')
expect(version).toHaveProperty('stable')
expect(version).toHaveProperty('downloadUrl')
expect(version).toHaveProperty('size')
expect(version).toHaveProperty('sha256')
// Verify types
expect(typeof version.version).toBe('string')
expect(typeof version.stable).toBe('boolean')
expect(typeof version.downloadUrl).toBe('string')
expect(typeof version.size).toBe('number')
expect(typeof version.sha256).toBe('string')
// Verify download URL format
expect(version.downloadUrl).toMatch(/^https:\/\/golang\.org\/dl\//)
})
// Verify only stable versions are returned
result.forEach(version => {
expect(version.stable).toBe(true)
})
// Verify result is limited to 20 versions
expect(result.length).toBeLessThanOrEqual(20)
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 10 })
})
test('Property 2: Download URL Construction', () => {
// **Feature: go-version-management, Property 2: Download URL Construction**
fc.assert(fc.property(
fc.record({
version: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v)),
filename: fc.string({ minLength: 1 }).filter(f => f.endsWith('.zip')),
size: fc.nat({ min: 1 }),
sha256: fc.string({ minLength: 64, maxLength: 64 }).filter(s => /^[0-9a-f]+$/i.test(s))
}),
(goVersion) => {
const mockStore = new MockConfigStore()
try {
// Simulate constructing download URL
const downloadUrl = `https://golang.org/dl/${goVersion.filename}`
// Verify URL construction
expect(downloadUrl).toMatch(/^https:\/\/golang\.org\/dl\//)
expect(downloadUrl).toContain(goVersion.filename)
expect(downloadUrl).toMatch(/\.zip$/)
// Verify the URL is well-formed
expect(() => new URL(downloadUrl)).not.toThrow()
// Verify version information is preserved
expect(goVersion.version).toMatch(/^go\d+\.\d+\.\d+$/)
expect(goVersion.size).toBeGreaterThan(0)
expect(goVersion.sha256).toMatch(/^[0-9a-f]{64}$/i)
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 3: Download Progress Reporting', () => {
// **Feature: go-version-management, Property 3: Download Progress Reporting**
fc.assert(fc.property(
fc.record({
downloadedBytes: fc.nat(),
totalBytes: fc.nat({ min: 1 }),
percent: fc.integer({ min: 0, max: 100 })
}).filter(progress => progress.downloadedBytes <= progress.totalBytes),
(progressData) => {
const mockStore = new MockConfigStore()
try {
// Simulate progress reporting structure
const progress = {
percent: progressData.percent,
downloadedBytes: progressData.downloadedBytes,
totalBytes: progressData.totalBytes,
speed: progressData.downloadedBytes > 0 ? progressData.downloadedBytes / 1000 : 0
}
// Verify progress data structure
expect(progress).toHaveProperty('percent')
expect(progress).toHaveProperty('downloadedBytes')
expect(progress).toHaveProperty('totalBytes')
expect(progress).toHaveProperty('speed')
// Verify data types
expect(typeof progress.percent).toBe('number')
expect(typeof progress.downloadedBytes).toBe('number')
expect(typeof progress.totalBytes).toBe('number')
expect(typeof progress.speed).toBe('number')
// Verify value ranges
expect(progress.percent).toBeGreaterThanOrEqual(0)
expect(progress.percent).toBeLessThanOrEqual(100)
expect(progress.downloadedBytes).toBeGreaterThanOrEqual(0)
expect(progress.totalBytes).toBeGreaterThan(0)
expect(progress.downloadedBytes).toBeLessThanOrEqual(progress.totalBytes)
expect(progress.speed).toBeGreaterThanOrEqual(0)
// Verify percentage calculation consistency
if (progress.totalBytes > 0) {
const calculatedPercent = Math.round((progress.downloadedBytes / progress.totalBytes) * 100)
// Allow some tolerance for rounding differences
expect(Math.abs(progress.percent - calculatedPercent)).toBeLessThanOrEqual(1)
}
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 4: Installation Validation Completeness', () => {
// **Feature: go-version-management, Property 4: Installation Validation Completeness**
fc.assert(fc.property(
fc.record({
version: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v)),
hasGoExe: fc.boolean(),
hasGofmtExe: fc.boolean(),
hasValidVersion: fc.boolean(),
hasSrcDir: fc.boolean()
}),
(installationData) => {
const mockStore = new MockConfigStore()
try {
// Simulate installation validation components
const validationChecks = {
goExeExists: installationData.hasGoExe,
gofmtExeExists: installationData.hasGofmtExe,
versionMatches: installationData.hasValidVersion,
srcDirExists: installationData.hasSrcDir
}
// For a complete installation, all checks should pass
const isCompleteInstallation = Object.values(validationChecks).every(check => check === true)
// Verify validation structure
expect(validationChecks).toHaveProperty('goExeExists')
expect(validationChecks).toHaveProperty('gofmtExeExists')
expect(validationChecks).toHaveProperty('versionMatches')
expect(validationChecks).toHaveProperty('srcDirExists')
// Verify types
expect(typeof validationChecks.goExeExists).toBe('boolean')
expect(typeof validationChecks.gofmtExeExists).toBe('boolean')
expect(typeof validationChecks.versionMatches).toBe('boolean')
expect(typeof validationChecks.srcDirExists).toBe('boolean')
// Verify version format
expect(installationData.version).toMatch(/^go\d+\.\d+\.\d+$/)
// If installation is complete, all validation checks should pass
if (isCompleteInstallation) {
expect(validationChecks.goExeExists).toBe(true)
expect(validationChecks.gofmtExeExists).toBe(true)
expect(validationChecks.versionMatches).toBe(true)
expect(validationChecks.srcDirExists).toBe(true)
}
// If any critical component is missing, installation should be considered incomplete
if (!validationChecks.goExeExists || !validationChecks.gofmtExeExists) {
expect(isCompleteInstallation).toBe(false)
}
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 5: Duplicate Installation Prevention', () => {
// **Feature: go-version-management, Property 5: Duplicate Installation Prevention**
fc.assert(fc.property(
fc.record({
version: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v)),
isAlreadyInstalled: fc.boolean(),
installationAttempts: fc.integer({ min: 1, max: 5 })
}),
(installationData) => {
const mockStore = new MockConfigStore()
try {
// Simulate installation state
const installedVersions = installationData.isAlreadyInstalled
? [installationData.version]
: []
// Simulate multiple installation attempts
const installationResults = []
for (let i = 0; i < installationData.installationAttempts; i++) {
const isVersionInstalled = installedVersions.includes(installationData.version)
const result = {
success: !isVersionInstalled,
message: isVersionInstalled
? `Go ${installationData.version} 已安装,无需重复安装`
: `Go ${installationData.version} 安装成功`,
attempt: i + 1
}
installationResults.push(result)
// If installation was successful, add to installed versions
if (result.success) {
installedVersions.push(installationData.version)
}
}
// Verify installation results structure
installationResults.forEach(result => {
expect(result).toHaveProperty('success')
expect(result).toHaveProperty('message')
expect(result).toHaveProperty('attempt')
expect(typeof result.success).toBe('boolean')
expect(typeof result.message).toBe('string')
expect(typeof result.attempt).toBe('number')
expect(result.message.length).toBeGreaterThan(0)
})
// Verify duplicate installation prevention
if (installationData.isAlreadyInstalled) {
// If version was already installed, all attempts should fail
installationResults.forEach(result => {
expect(result.success).toBe(false)
expect(result.message).toContain('已安装')
})
} else {
// If version wasn't installed, first attempt should succeed, rest should fail
expect(installationResults[0].success).toBe(true)
expect(installationResults[0].message).toContain('安装成功')
// Subsequent attempts should fail due to duplicate prevention
for (let i = 1; i < installationResults.length; i++) {
expect(installationResults[i].success).toBe(false)
expect(installationResults[i].message).toContain('已安装')
}
}
// Verify version should only appear once in installed versions list
const versionCount = installedVersions.filter(v => v === installationData.version).length
expect(versionCount).toBeLessThanOrEqual(1)
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 11: Environment Variable Validation', () => {
// **Feature: go-version-management, Property 11: Environment Variable Validation**
fc.assert(fc.property(
fc.record({
version: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v)),
goroot: fc.string({ minLength: 1 }),
gopath: fc.string({ minLength: 1 }),
goBin: fc.string({ minLength: 1 }),
systemGoVersionOutput: fc.string({ minLength: 1 }),
systemGoRootOutput: fc.string({ minLength: 1 })
}),
(validationData) => {
const mockStore = new MockConfigStore()
try {
// Simulate environment variable validation after version switch
const expectedGoRoot = validationData.goroot
const expectedGoPath = validationData.gopath
const expectedGoBin = validationData.goBin
// Simulate system command outputs
const goVersionMatches = validationData.systemGoVersionOutput.includes(validationData.version)
const goRootMatches = validationData.systemGoRootOutput.trim() === expectedGoRoot
// Simulate validation results
const validationResult = {
environmentVariables: {
GOROOT: expectedGoRoot,
GOPATH: expectedGoPath,
PATH: `${expectedGoBin};C:\\Windows\\System32`
},
systemValidation: {
goVersionOutput: validationData.systemGoVersionOutput,
goRootOutput: validationData.systemGoRootOutput,
goVersionMatches,
goRootMatches
},
errors: [] as string[]
}
// Perform validation checks
if (!goVersionMatches) {
validationResult.errors.push(`go version 输出不匹配: 期望包含 '${validationData.version}', 实际 '${validationData.systemGoVersionOutput}'`)
}
if (!goRootMatches) {
validationResult.errors.push(`GOROOT 不匹配: 期望 '${expectedGoRoot}', 实际 '${validationData.systemGoRootOutput.trim()}'`)
}
// Check if PATH contains Go binary
const pathContainsGoBin = validationResult.environmentVariables.PATH.includes(expectedGoBin)
if (!pathContainsGoBin) {
validationResult.errors.push(`PATH 中缺少 Go 二进制路径: '${expectedGoBin}'`)
}
const isValid = validationResult.errors.length === 0
// Verify validation result structure
expect(validationResult).toHaveProperty('environmentVariables')
expect(validationResult).toHaveProperty('systemValidation')
expect(validationResult).toHaveProperty('errors')
// Verify environment variables structure
expect(validationResult.environmentVariables).toHaveProperty('GOROOT')
expect(validationResult.environmentVariables).toHaveProperty('GOPATH')
expect(validationResult.environmentVariables).toHaveProperty('PATH')
// Verify system validation structure
expect(validationResult.systemValidation).toHaveProperty('goVersionOutput')
expect(validationResult.systemValidation).toHaveProperty('goRootOutput')
expect(validationResult.systemValidation).toHaveProperty('goVersionMatches')
expect(validationResult.systemValidation).toHaveProperty('goRootMatches')
// Verify types
expect(typeof validationResult.environmentVariables.GOROOT).toBe('string')
expect(typeof validationResult.environmentVariables.GOPATH).toBe('string')
expect(typeof validationResult.environmentVariables.PATH).toBe('string')
expect(typeof validationResult.systemValidation.goVersionMatches).toBe('boolean')
expect(typeof validationResult.systemValidation.goRootMatches).toBe('boolean')
expect(Array.isArray(validationResult.errors)).toBe(true)
// Verify validation logic
if (goVersionMatches && goRootMatches && pathContainsGoBin) {
expect(isValid).toBe(true)
expect(validationResult.errors.length).toBe(0)
} else {
expect(isValid).toBe(false)
expect(validationResult.errors.length).toBeGreaterThan(0)
}
// Verify error messages are descriptive
validationResult.errors.forEach(error => {
expect(typeof error).toBe('string')
expect(error.length).toBeGreaterThan(0)
expect(error).toMatch(/期望|实际|不匹配|缺少/)
})
// Verify PATH structure
const pathEntries = validationResult.environmentVariables.PATH.split(';')
expect(pathEntries.length).toBeGreaterThan(0)
if (pathContainsGoBin) {
expect(pathEntries).toContain(expectedGoBin)
}
// Verify version format in validation
expect(validationData.version).toMatch(/^go\d+\.\d+\.\d+$/)
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 7: Active Version Environment Management', () => {
// **Feature: go-version-management, Property 7: Active Version Environment Management**
fc.assert(fc.property(
fc.record({
version: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v)),
goroot: fc.string({ minLength: 1 }),
gopath: fc.string({ minLength: 1 }),
goBin: fc.string({ minLength: 1 }),
existingPathEntries: fc.array(fc.string({ minLength: 1 })),
hasOldGoPaths: fc.boolean()
}),
(envData) => {
const mockStore = new MockConfigStore()
try {
// Simulate environment variable update process
const currentPath = envData.existingPathEntries.join(';')
// Add some old Go paths if specified
let pathWithOldGo = currentPath
if (envData.hasOldGoPaths) {
pathWithOldGo = `${currentPath};C:\\go\\go-1.20.0\\bin;C:\\go\\go-1.19.0\\bin`
}
// Simulate PATH cleaning and updating
const pathArray = pathWithOldGo.split(';').filter(p => p.trim() !== '')
// Remove old Go paths (simulate the cleaning logic)
const filteredPaths = pathArray.filter(p =>
!p.includes('\\go\\go-') &&
!p.includes('\\go-') &&
!p.includes('\\golang\\') &&
!p.includes('\\Go\\')
)
// Add new Go path
const newPathArray = [envData.goBin, ...filteredPaths]
const finalPath = newPathArray.filter(p => p.trim() !== '').join(';')
// Simulate environment variables
const environmentVariables = {
GOROOT: envData.goroot,
GOPATH: envData.gopath,
PATH: finalPath
}
// Verify environment variable structure
expect(environmentVariables).toHaveProperty('GOROOT')
expect(environmentVariables).toHaveProperty('GOPATH')
expect(environmentVariables).toHaveProperty('PATH')
// Verify types
expect(typeof environmentVariables.GOROOT).toBe('string')
expect(typeof environmentVariables.GOPATH).toBe('string')
expect(typeof environmentVariables.PATH).toBe('string')
// Verify values are non-empty
expect(environmentVariables.GOROOT.length).toBeGreaterThan(0)
expect(environmentVariables.GOPATH.length).toBeGreaterThan(0)
expect(environmentVariables.PATH.length).toBeGreaterThan(0)
// Verify PATH contains the new Go binary path
expect(environmentVariables.PATH).toContain(envData.goBin)
// Verify old Go paths are removed
const pathEntries = environmentVariables.PATH.split(';')
const hasOldGoPaths = pathEntries.some(p =>
(p.includes('\\go\\go-') || p.includes('\\go-')) && p !== envData.goBin
)
expect(hasOldGoPaths).toBe(false)
// Verify Go binary path appears only once
const goPathCount = pathEntries.filter(p => p === envData.goBin).length
expect(goPathCount).toBe(1)
// Verify Go binary path is at the beginning (highest priority)
expect(pathEntries[0]).toBe(envData.goBin)
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 14: GOPATH Default Configuration', () => {
// **Feature: go-version-management, Property 14: GOPATH Default Configuration**
fc.assert(fc.property(
fc.record({
basePath: fc.string({ minLength: 1 }),
version: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v))
}),
(configData) => {
const mockStore = new MockConfigStore()
try {
// Simulate GOPATH configuration logic
const goBasePath = `${configData.basePath}\\go`
const defaultGoPath = `${goBasePath}\\workspace`
// Simulate GOPATH workspace structure
const workspaceStructure = {
gopath: defaultGoPath,
subdirectories: ['src', 'pkg', 'bin'],
fullPaths: [
`${defaultGoPath}\\src`,
`${defaultGoPath}\\pkg`,
`${defaultGoPath}\\bin`
]
}
// Verify GOPATH structure
expect(workspaceStructure).toHaveProperty('gopath')
expect(workspaceStructure).toHaveProperty('subdirectories')
expect(workspaceStructure).toHaveProperty('fullPaths')
// Verify types
expect(typeof workspaceStructure.gopath).toBe('string')
expect(Array.isArray(workspaceStructure.subdirectories)).toBe(true)
expect(Array.isArray(workspaceStructure.fullPaths)).toBe(true)
// Verify GOPATH format
expect(workspaceStructure.gopath).toContain('workspace')
expect(workspaceStructure.gopath.length).toBeGreaterThan(0)
// Verify standard Go workspace subdirectories
expect(workspaceStructure.subdirectories).toContain('src')
expect(workspaceStructure.subdirectories).toContain('pkg')
expect(workspaceStructure.subdirectories).toContain('bin')
expect(workspaceStructure.subdirectories.length).toBe(3)
// Verify full paths are correctly constructed
workspaceStructure.fullPaths.forEach(path => {
expect(path).toContain(workspaceStructure.gopath)
expect(typeof path).toBe('string')
expect(path.length).toBeGreaterThan(workspaceStructure.gopath.length)
})
// Verify each subdirectory has a corresponding full path
workspaceStructure.subdirectories.forEach(subdir => {
const expectedPath = `${workspaceStructure.gopath}\\${subdir}`
expect(workspaceStructure.fullPaths).toContain(expectedPath)
})
// Verify GOPATH is under the Go base directory
expect(workspaceStructure.gopath).toContain(goBasePath)
// Verify GOPATH uses the standard workspace directory name
expect(workspaceStructure.gopath.endsWith('workspace')).toBe(true)
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 6: File System Cleanup on Uninstall', () => {
// **Feature: go-version-management, Property 6: File System Cleanup on Uninstall**
fc.assert(fc.property(
fc.record({
version: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v)),
versionDir: fc.string({ minLength: 1 }),
fileCount: fc.nat({ min: 1, max: 100 }),
dirCount: fc.nat({ min: 1, max: 20 }),
totalSize: fc.nat({ min: 1000, max: 1000000 }),
uninstallSuccess: fc.boolean()
}),
(uninstallData) => {
const mockStore = new MockConfigStore()
try {
// Simulate uninstall operation and file system cleanup
const preUninstallState = {
versionExists: true,
directoryPath: uninstallData.versionDir,
fileCount: uninstallData.fileCount,
dirCount: uninstallData.dirCount,
totalSize: uninstallData.totalSize
}
// Simulate uninstall result
const uninstallResult = {
success: uninstallData.uninstallSuccess,
message: uninstallData.uninstallSuccess
? `Go ${uninstallData.version} 已成功卸载`
: `卸载失败: 文件系统清理失败`,
filesRemoved: uninstallData.uninstallSuccess ? uninstallData.fileCount : 0,
dirsRemoved: uninstallData.uninstallSuccess ? uninstallData.dirCount : 0,
bytesFreed: uninstallData.uninstallSuccess ? uninstallData.totalSize : 0
}
// Simulate post-uninstall state
const postUninstallState = {
versionExists: !uninstallData.uninstallSuccess,
directoryPath: uninstallData.versionDir,
fileCount: uninstallData.uninstallSuccess ? 0 : uninstallData.fileCount,
dirCount: uninstallData.uninstallSuccess ? 0 : uninstallData.dirCount,
totalSize: uninstallData.uninstallSuccess ? 0 : uninstallData.totalSize
}
// Verify uninstall result structure
expect(uninstallResult).toHaveProperty('success')
expect(uninstallResult).toHaveProperty('message')
expect(uninstallResult).toHaveProperty('filesRemoved')
expect(uninstallResult).toHaveProperty('dirsRemoved')
expect(uninstallResult).toHaveProperty('bytesFreed')
// Verify types
expect(typeof uninstallResult.success).toBe('boolean')
expect(typeof uninstallResult.message).toBe('string')
expect(typeof uninstallResult.filesRemoved).toBe('number')
expect(typeof uninstallResult.dirsRemoved).toBe('number')
expect(typeof uninstallResult.bytesFreed).toBe('number')
// Verify message is descriptive
expect(uninstallResult.message.length).toBeGreaterThan(0)
expect(uninstallResult.message).toContain(uninstallData.version)
// Verify cleanup completeness for successful uninstall
if (uninstallData.uninstallSuccess) {
expect(uninstallResult.success).toBe(true)
expect(uninstallResult.message).toContain('成功卸载')
expect(uninstallResult.filesRemoved).toBe(preUninstallState.fileCount)
expect(uninstallResult.dirsRemoved).toBe(preUninstallState.dirCount)
expect(uninstallResult.bytesFreed).toBe(preUninstallState.totalSize)
// Post-uninstall state should show complete cleanup
expect(postUninstallState.versionExists).toBe(false)
expect(postUninstallState.fileCount).toBe(0)
expect(postUninstallState.dirCount).toBe(0)
expect(postUninstallState.totalSize).toBe(0)
} else {
expect(uninstallResult.success).toBe(false)
expect(uninstallResult.message).toContain('卸载失败')
expect(uninstallResult.filesRemoved).toBe(0)
expect(uninstallResult.dirsRemoved).toBe(0)
expect(uninstallResult.bytesFreed).toBe(0)
// Post-uninstall state should show no changes
expect(postUninstallState.versionExists).toBe(true)
expect(postUninstallState.fileCount).toBe(preUninstallState.fileCount)
expect(postUninstallState.dirCount).toBe(preUninstallState.dirCount)
expect(postUninstallState.totalSize).toBe(preUninstallState.totalSize)
}
// Verify numeric values are non-negative
expect(uninstallResult.filesRemoved).toBeGreaterThanOrEqual(0)
expect(uninstallResult.dirsRemoved).toBeGreaterThanOrEqual(0)
expect(uninstallResult.bytesFreed).toBeGreaterThanOrEqual(0)
// Verify version format
expect(uninstallData.version).toMatch(/^go\d+\.\d+\.\d+$/)
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 8: Version State Consistency', () => {
// **Feature: go-version-management, Property 8: Version State Consistency**
fc.assert(fc.property(
fc.record({
initialVersions: fc.array(fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v))),
versionToUninstall: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v)),
wasActiveVersion: fc.boolean(),
uninstallSuccess: fc.boolean()
}),
(stateData) => {
const mockStore = new MockConfigStore()
try {
// Simulate initial state
const initialState = {
installedVersions: [...stateData.initialVersions],
activeVersion: stateData.wasActiveVersion ? stateData.versionToUninstall : '',
versionExists: stateData.initialVersions.includes(stateData.versionToUninstall)
}
// Simulate uninstall operation
const uninstallOperation = {
targetVersion: stateData.versionToUninstall,
wasActive: stateData.wasActiveVersion && initialState.versionExists,
success: stateData.uninstallSuccess && initialState.versionExists
}
// Simulate post-uninstall state
const postUninstallState = {
installedVersions: uninstallOperation.success
? initialState.installedVersions.filter(v => v !== stateData.versionToUninstall)
: [...initialState.installedVersions],
activeVersion: (uninstallOperation.success && uninstallOperation.wasActive)
? ''
: initialState.activeVersion,
versionExists: !uninstallOperation.success && initialState.versionExists
}
// Verify state consistency
expect(initialState).toHaveProperty('installedVersions')
expect(initialState).toHaveProperty('activeVersion')
expect(initialState).toHaveProperty('versionExists')
expect(postUninstallState).toHaveProperty('installedVersions')
expect(postUninstallState).toHaveProperty('activeVersion')
expect(postUninstallState).toHaveProperty('versionExists')
// Verify types
expect(Array.isArray(initialState.installedVersions)).toBe(true)
expect(Array.isArray(postUninstallState.installedVersions)).toBe(true)
expect(typeof initialState.activeVersion).toBe('string')
expect(typeof postUninstallState.activeVersion).toBe('string')
expect(typeof initialState.versionExists).toBe('boolean')
expect(typeof postUninstallState.versionExists).toBe('boolean')
// Verify version format
expect(stateData.versionToUninstall).toMatch(/^go\d+\.\d+\.\d+$/)
initialState.installedVersions.forEach(version => {
expect(version).toMatch(/^go\d+\.\d+\.\d+$/)
})
postUninstallState.installedVersions.forEach(version => {
expect(version).toMatch(/^go\d+\.\d+\.\d+$/)
})
// Verify state transitions for successful uninstall
if (uninstallOperation.success) {
// Version should be removed from installed versions list
expect(postUninstallState.installedVersions).not.toContain(stateData.versionToUninstall)
expect(postUninstallState.installedVersions.length).toBe(
Math.max(0, initialState.installedVersions.length - 1)
)
// If uninstalled version was active, active version should be cleared
if (uninstallOperation.wasActive) {
expect(postUninstallState.activeVersion).toBe('')
}
// Version should no longer exist
expect(postUninstallState.versionExists).toBe(false)
} else {
// State should remain unchanged for failed uninstall
expect(postUninstallState.installedVersions).toEqual(initialState.installedVersions)
expect(postUninstallState.activeVersion).toBe(initialState.activeVersion)
expect(postUninstallState.versionExists).toBe(initialState.versionExists)
}
// Verify active version consistency
if (postUninstallState.activeVersion !== '') {
expect(postUninstallState.installedVersions).toContain(postUninstallState.activeVersion)
}
// Verify no duplicate versions in installed list
const uniqueVersions = [...new Set(postUninstallState.installedVersions)]
expect(uniqueVersions.length).toBe(postUninstallState.installedVersions.length)
// Verify uninstall operation properties
expect(typeof uninstallOperation.success).toBe('boolean')
expect(typeof uninstallOperation.wasActive).toBe('boolean')
// If version didn't exist initially, uninstall should fail
if (!initialState.versionExists) {
expect(uninstallOperation.success).toBe(false)
}
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 100 })
})
test('Property 9: Version Information Completeness', () => {
// **Feature: go-version-management, Property 9: Version Information Completeness**
fc.assert(fc.property(
fc.array(fc.record({
version: fc.string({ minLength: 1 }).filter(v => /^go\d+\.\d+\.\d+$/.test(v)),
path: fc.string({ minLength: 1 }),
isActive: fc.boolean(),
goroot: fc.string({ minLength: 1 }),
gopath: fc.string({ minLength: 1 })
})),
(mockVersions) => {
const mockStore = new MockConfigStore()
try {
// Verify all version objects have required fields
mockVersions.forEach(version => {
expect(version).toHaveProperty('version')
expect(version).toHaveProperty('path')
expect(version).toHaveProperty('isActive')
expect(version).toHaveProperty('goroot')
expect(version).toHaveProperty('gopath')
// Verify types
expect(typeof version.version).toBe('string')
expect(typeof version.path).toBe('string')
expect(typeof version.isActive).toBe('boolean')
expect(typeof version.goroot).toBe('string')
expect(typeof version.gopath).toBe('string')
// Verify version format
expect(version.version).toMatch(/^go\d+\.\d+\.\d+$/)
// Verify paths are non-empty
expect(version.path.length).toBeGreaterThan(0)
expect(version.goroot.length).toBeGreaterThan(0)
expect(version.gopath.length).toBeGreaterThan(0)
})
// Verify at most one version can be active
const activeVersions = mockVersions.filter(v => v.isActive)
expect(activeVersions.length).toBeLessThanOrEqual(1)
return true
} finally {
mockStore.cleanup()
}
}
), { numRuns: 10 })
})
})