- 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
1065 lines
41 KiB
TypeScript
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 })
|
|
})
|
|
}) |