From 47778402e6d6b97ac1dd980d6f7114e3ded730d3 Mon Sep 17 00:00:00 2001 From: Satyaki Ghosh Date: Mon, 8 Dec 2025 19:45:55 -0500 Subject: [PATCH 1/2] Improve tests --- .github/workflows/ci.yml | 2 +- tst/app/standalone.test.ts | 7 - .../ResourceEntityCompletionProvider.test.ts | 75 ++---- ...ResourcePropertyCompletionProvider.test.ts | 177 +++---------- .../ResourceSectionCompletionProvider.test.ts | 75 +----- .../ResourceStateCompletionProvider.test.ts | 242 +++++++----------- .../ResourceTypeCompletionProvider.test.ts | 39 +-- tst/unit/handlers/StackHandler.test.ts | 11 +- .../RelatedResourcesSnippetProvider.test.ts | 45 ++-- .../ResourceStateImporter.test.ts | 3 +- .../cfnLint/PyodideWorkerManager.test.ts | 228 ++--------------- tst/unit/tools/wheel-download.test.ts | 48 ---- 12 files changed, 207 insertions(+), 745 deletions(-) delete mode 100644 tst/app/standalone.test.ts delete mode 100644 tst/unit/tools/wheel-download.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfd3c1bf..99b2b1b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ ubuntu-latest, macos-latest ] + os: [ ubuntu-latest, macos-latest, windows-latest ] with: ref: ${{ github.sha }} runs-on: ${{ matrix.os }} diff --git a/tst/app/standalone.test.ts b/tst/app/standalone.test.ts deleted file mode 100644 index 17a1dff9..00000000 --- a/tst/app/standalone.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, test, expect } from 'vitest'; - -describe('Standalone', () => { - test('Some test', () => { - expect(1).toBe(1); - }); -}); diff --git a/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts index 596db930..32d8b0f2 100644 --- a/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts @@ -3,12 +3,11 @@ import { CompletionParams, CompletionItemKind, CompletionItem, InsertTextFormat import { ResourceEntityCompletionProvider } from '../../../src/autocomplete/ResourceEntityCompletionProvider'; import { ResourceAttribute } from '../../../src/context/ContextType'; import { YamlNodeTypes } from '../../../src/context/syntaxtree/utils/TreeSitterTypes'; -import { CombinedSchemas } from '../../../src/schema/CombinedSchemas'; import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { ExtensionName } from '../../../src/utils/ExtensionConfig'; import { createForEachResourceContext, createResourceContext } from '../../utils/MockContext'; import { createMockComponents } from '../../utils/MockServerComponents'; -import { combinedSchemas } from '../../utils/SchemaUtils'; +import { combinedSchemas, combineSchema, Schemas } from '../../utils/SchemaUtils'; describe('ResourceEntityCompletionProvider', () => { const mockComponents = createMockComponents(); @@ -22,6 +21,18 @@ describe('ResourceEntityCompletionProvider', () => { position: { line: 0, character: 0 }, }; + // Create schemas once at describe level + const ec2Schema = new ResourceSchema(Schemas.EC2Instance.contents); + const ec2WithRequiredProps = combineSchema(ec2Schema, 'AWS::EC2::Instance', { + required: ['InstanceType', 'ImageId'], + }); + const ec2WithNoRequiredProps = combineSchema(ec2Schema, 'AWS::EC2::Instance', { + required: [], + }); + const schemasWithRequired = combinedSchemas([ec2WithRequiredProps]); + const schemasWithNoRequired = combinedSchemas([ec2WithNoRequiredProps]); + const emptySchemas = combinedSchemas([]); + beforeEach(() => { mockComponents.schemaRetriever.getDefault.reset(); }); @@ -123,30 +134,6 @@ describe('ResourceEntityCompletionProvider', () => { expect(deletionPolicyItem!.insertText).toBe('DeletionPolicy'); }); - // Create a mock schema with required properties for testing - const setupSchemaWithRequiredProps = (requiredProps: string[] = []) => { - const mockSchema = { - typeName: 'AWS::EC2::Instance', - propertyKeys: new Set(['InstanceType', 'ImageId', 'KeyName', 'SecurityGroups']), - required: requiredProps, - isReadOnly: () => false, - isRequired: (prop: string) => requiredProps.includes(prop), - getByPath: () => ({ type: 'string' }), - resolveRef: () => ({ type: 'string' }), - } as unknown as ResourceSchema; - - const mockSchemas = new Map(); - mockSchemas.set('AWS::EC2::Instance', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - return { mockSchema, combinedSchemas }; - }; - test('should enhance Properties completion with snippet when resource type is available', () => { // Setup context with a resource that has a Type const mockContext = createResourceContext('MyInstance', { @@ -158,7 +145,7 @@ describe('ResourceEntityCompletionProvider', () => { }); // Setup schema with required properties - setupSchemaWithRequiredProps(['InstanceType', 'ImageId']); + mockComponents.schemaRetriever.getDefault.returns(schemasWithRequired); // Get completions const result = provider.getCompletions(mockContext, mockParams); @@ -187,7 +174,7 @@ describe('ResourceEntityCompletionProvider', () => { }); // Setup schema with required properties - setupSchemaWithRequiredProps(['InstanceType', 'ImageId']); + mockComponents.schemaRetriever.getDefault.returns(schemasWithRequired); // Get completions const result = provider.getCompletions(mockContext, mockParams); @@ -214,7 +201,7 @@ describe('ResourceEntityCompletionProvider', () => { }); // Setup schema with no required properties - setupSchemaWithRequiredProps([]); + mockComponents.schemaRetriever.getDefault.returns(schemasWithNoRequired); // Get completions const result = provider.getCompletions(mockContext, mockParams); @@ -259,8 +246,7 @@ describe('ResourceEntityCompletionProvider', () => { }); // Setup empty schemas - const testSchemas = combinedSchemas([]); - mockComponents.schemaRetriever.getDefault.returns(testSchemas); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); // Get completions const result = provider.getCompletions(mockContext, mockParams); @@ -307,29 +293,7 @@ describe('ResourceEntityCompletionProvider', () => { }, }); - const setupSchemaWithRequiredProps = () => { - const mockSchema = { - typeName: 'AWS::EC2::Instance', - propertyKeys: new Set(['InstanceType', 'ImageId']), - required: ['InstanceType', 'ImageId'], - isReadOnly: () => false, - isRequired: (prop: string) => ['InstanceType', 'ImageId'].includes(prop), - getByPath: () => ({ type: 'string' }), - resolveRef: () => ({ type: 'string' }), - } as unknown as ResourceSchema; - - const mockSchemas = new Map(); - mockSchemas.set('AWS::EC2::Instance', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - }; - - setupSchemaWithRequiredProps(); + mockComponents.schemaRetriever.getDefault.returns(schemasWithRequired); const result = provider.getCompletions(mockContext, mockParams); @@ -381,8 +345,7 @@ describe('ResourceEntityCompletionProvider', () => { }, }); - const testSchemas = combinedSchemas([]); - mockComponents.schemaRetriever.getDefault.returns(testSchemas); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); const result = provider.getCompletions(mockContext, mockParams); diff --git a/tst/unit/autocomplete/ResourcePropertyCompletionProvider.test.ts b/tst/unit/autocomplete/ResourcePropertyCompletionProvider.test.ts index 619af006..73248359 100644 --- a/tst/unit/autocomplete/ResourcePropertyCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourcePropertyCompletionProvider.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test, beforeEach, vi } from 'vitest'; import { CompletionParams, CompletionItemKind } from 'vscode-languageserver'; import { ResourcePropertyCompletionProvider } from '../../../src/autocomplete/ResourcePropertyCompletionProvider'; -import { CombinedSchemas } from '../../../src/schema/CombinedSchemas'; import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { ExtensionName } from '../../../src/utils/ExtensionConfig'; import { @@ -9,13 +8,22 @@ import { createForEachResourceContext, createResourceContext, } from '../../utils/MockContext'; -import { createMockComponents } from '../../utils/MockServerComponents'; +import { createMockComponents, createMockSchemaRetriever } from '../../utils/MockServerComponents'; import { Schemas, combinedSchemas } from '../../utils/SchemaUtils'; describe('ResourcePropertyCompletionProvider', () => { - const mockComponents = createMockComponents(); + const s3Schemas = combinedSchemas([Schemas.S3Bucket]); + const emptySchemas = combinedSchemas([]); + const mockComponents = createMockComponents({ + schemaRetriever: createMockSchemaRetriever(s3Schemas), + }); const provider = new ResourcePropertyCompletionProvider(mockComponents.schemaRetriever); + beforeEach(() => { + mockComponents.schemaRetriever.getDefault.returns(s3Schemas); + emptySchemas.schemas.clear(); + }); + const mockParams: CompletionParams = { textDocument: { uri: 'file:///test.yaml' }, position: { line: 0, character: 0 }, @@ -30,19 +38,8 @@ describe('ResourcePropertyCompletionProvider', () => { }, }; - const setupS3Schema = () => { - const testSchemas = combinedSchemas([Schemas.S3Bucket]); - mockComponents.schemaRetriever.getDefault.returns(testSchemas); - return testSchemas; - }; - - beforeEach(() => { - mockComponents.schemaRetriever.getDefault.reset(); - }); - test('should return all optional properties when inside Properties section with empty text for no required properties', () => { const mockContext = createResourceContext('MyBucket', s3BucketContext); - setupS3Schema(); const result = provider.getCompletions(mockContext, mockParams); @@ -61,7 +58,6 @@ describe('ResourcePropertyCompletionProvider', () => { text: 'Bucket', propertyPath: ['Resources', 'MyBucket', 'Properties', 'Bucket'], }); - setupS3Schema(); const result = provider.getCompletions(mockContext, mockParams); @@ -81,16 +77,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should exclude already defined properties from completions', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - const context = createContextFromYamlContentAndPath( `Resources: MyBucket: @@ -114,16 +100,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should exclude existing properties from nested object completions', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - // Create a resource with nested properties already defined const mockContext = createResourceContext('MyBucket', { text: '', @@ -146,16 +122,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should handle deeply nested existing properties correctly', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - // Create a resource with deeply nested properties const mockContext = createResourceContext('MyBucket', { text: '', @@ -186,16 +152,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should handle missing nested objects gracefully', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - // Create a resource where the nested path doesn't exist const mockContext = createResourceContext('MyBucket', { text: '', @@ -216,16 +172,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should handle array indices in property path correctly', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - // Create a resource with array properties const mockContext = createResourceContext('MyBucket', { text: '', @@ -275,7 +221,6 @@ describe('ResourcePropertyCompletionProvider', () => { text: 'BucketName', // Use exact property name to avoid fuzzy search issues propertyPath: ['Resources', 'MyBucket', 'Properties', 'BucketName'], }); - setupS3Schema(); const result = provider.getCompletions(mockContext, mockParams); @@ -292,16 +237,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should include all properties when none are defined', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - // Create a resource with no properties defined const mockContext = createResourceContext('MyBucket', { text: 'B', @@ -332,7 +267,6 @@ describe('ResourcePropertyCompletionProvider', () => { Properties: { Bucket: 'some-value' }, }, }); - setupS3Schema(); const result = provider.getCompletions(mockContext, mockParams); @@ -371,7 +305,6 @@ describe('ResourcePropertyCompletionProvider', () => { text: 'A', // Provide text to get completions propertyPath: ['Resources', 'MyBucket', 'Properties', 'A'], }); - setupS3Schema(); const result = provider.getCompletions(mockContext, mockParams); @@ -424,13 +357,11 @@ describe('ResourcePropertyCompletionProvider', () => { const mockSchemas = new Map(); mockSchemas.set('AWS::S3::Bucket', modifiedSchema); - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); + const schemas = emptySchemas; + for (const [k, v] of mockSchemas.entries()) schemas.schemas.set(k, v); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - return combinedSchemas; + mockComponents.schemaRetriever.getDefault.returns(schemas); + return schemas; } setupSchemaWithRequiredProps(); @@ -467,8 +398,6 @@ describe('ResourcePropertyCompletionProvider', () => { const originalAtBlockMappingLevel = mockContext.atBlockMappingLevel; mockContext.atBlockMappingLevel = vi.fn().mockReturnValue(true); - setupS3Schema(); - const result = provider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); @@ -501,8 +430,6 @@ describe('ResourcePropertyCompletionProvider', () => { // Mock the isBlockMapping method to return false (positioned on a specific node) mockContext.atBlockMappingLevel = () => false; - setupS3Schema(); - const result = provider.getCompletions(mockContext, mockParams); expect(result).toBeDefined(); @@ -570,13 +497,11 @@ describe('ResourcePropertyCompletionProvider', () => { const mockSchemas = new Map(); mockSchemas.set('AWS::S3::Bucket', modifiedSchema); - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); + const schemas = emptySchemas; + for (const [k, v] of mockSchemas.entries()) schemas.schemas.set(k, v); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - return combinedSchemas; + mockComponents.schemaRetriever.getDefault.returns(schemas); + return schemas; } setupSchemaWithMixedProps(); @@ -657,12 +582,10 @@ describe('ResourcePropertyCompletionProvider', () => { const mockSchemas = new Map(); mockSchemas.set('AWS::S3::Bucket', mockSchema); - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); + const schemas = emptySchemas; + for (const [k, v] of mockSchemas.entries()) schemas.schemas.set(k, v); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); + mockComponents.schemaRetriever.getDefault.returns(schemas); const result = provider.getCompletions(mockContext, mockParams); @@ -735,12 +658,10 @@ describe('ResourcePropertyCompletionProvider', () => { const mockSchemas = new Map(); mockSchemas.set('AWS::S3::Bucket', mockSchema); - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); + const schemas = emptySchemas; + for (const [k, v] of mockSchemas.entries()) schemas.schemas.set(k, v); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); + mockComponents.schemaRetriever.getDefault.returns(schemas); const result = provider.getCompletions(mockContext, mockParams); @@ -758,16 +679,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should handle double quoted property names in YAML', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - const mockContext = createResourceContext('MyBucket', { text: `"Bucket"`, propertyPath: ['Resources', 'MyBucket', 'Properties', 'Bucket'], @@ -794,16 +705,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should handle single quoted property names in YAML', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - const mockContext = createResourceContext('MyBucket', { text: 'Bucket', propertyPath: ['Resources', 'MyBucket', 'Properties', 'Bucket'], @@ -887,12 +788,10 @@ describe('ResourcePropertyCompletionProvider', () => { const mockSchemas = new Map(); mockSchemas.set('AWS::S3::Bucket', mockSchema); - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); + const schemas = emptySchemas; + for (const [k, v] of mockSchemas.entries()) schemas.schemas.set(k, v); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); + mockComponents.schemaRetriever.getDefault.returns(schemas); return { mockSchema, combinedSchemas }; }; @@ -1713,7 +1612,6 @@ describe('ResourcePropertyCompletionProvider', () => { Properties: {}, }, }); - setupS3Schema(); const result = provider.getCompletions(mockContext, mockParams); @@ -1733,7 +1631,6 @@ describe('ResourcePropertyCompletionProvider', () => { Properties: {}, }, }); - setupS3Schema(); const result = provider.getCompletions(mockContext, mockParams); @@ -1748,16 +1645,6 @@ describe('ResourcePropertyCompletionProvider', () => { }); test('should handle nested properties in ForEach resources', () => { - const mockSchema = new ResourceSchema(Schemas.S3Bucket.contents); - const mockSchemas = new Map(); - mockSchemas.set('AWS::S3::Bucket', mockSchema); - - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - const mockContext = createForEachResourceContext('Fn::ForEach::Buckets', 'S3Bucket${BucketName}', { text: '', propertyPath: [ @@ -1824,11 +1711,9 @@ describe('ResourcePropertyCompletionProvider', () => { const mockSchemas = new Map(); mockSchemas.set('AWS::S3::Bucket', mockSchema); - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => mockSchemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); + const schemas = emptySchemas; + for (const [k, v] of mockSchemas.entries()) schemas.schemas.set(k, v); + mockComponents.schemaRetriever.getDefault.returns(schemas); const mockContext = createForEachResourceContext('Fn::ForEach::Buckets', 'S3Bucket${BucketName}', { text: '', diff --git a/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts index ae581b95..5ff77863 100644 --- a/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts @@ -1,8 +1,6 @@ import { describe, expect, test, beforeEach, vi } from 'vitest'; import { CompletionParams, CompletionItemKind, CompletionTriggerKind } from 'vscode-languageserver'; import { ResourceSectionCompletionProvider } from '../../../src/autocomplete/ResourceSectionCompletionProvider'; -import { CombinedSchemas } from '../../../src/schema/CombinedSchemas'; -import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { createResourceContext } from '../../utils/MockContext'; import { createMockComponents, @@ -10,6 +8,7 @@ import { createMockResourceStateManager, createMockSettingsManager, } from '../../utils/MockServerComponents'; +import { combinedSchemas, Schemas } from '../../utils/SchemaUtils'; describe('ResourceSectionCompletionProvider', () => { const mockComponents = createMockComponents(); @@ -41,63 +40,12 @@ describe('ResourceSectionCompletionProvider', () => { }, }; - // Common mock schemas - const createMockResourceSchemas = () => { - const mockS3Schema = { - typeName: 'AWS::S3::Bucket', - propertyKeys: new Set(['BucketName', 'AccessControl']), - isReadOnly: () => false, - isRequired: () => false, - getByPath: (path: string) => { - if (path === '/properties/AccessControl') { - return { type: 'string', enum: ['Private', 'PublicRead'] }; - } - return { type: 'string' }; - }, - resolveJsonPointerPath: (jsonPointerPath: string) => { - switch (jsonPointerPath) { - case '/properties/AccessControl': { - return [{ type: 'string', enum: ['Private', 'PublicRead'] }]; - } - case '/properties/BucketName': { - return [{ type: 'string' }]; - } - case '/properties': { - return [ - { - type: 'object', - properties: { - AccessControl: { type: 'string', enum: ['Private', 'PublicRead'] }, - BucketName: { type: 'string' }, - }, - }, - ]; - } - // No default - } - return []; - }, - } as unknown as ResourceSchema; - - const mockSchemas = new Map(); - mockSchemas.set('AWS::EC2::Instance', {} as ResourceSchema); - mockSchemas.set('AWS::S3::Bucket', mockS3Schema); - mockSchemas.set('AWS::S3::BucketPolicy', {} as ResourceSchema); - mockSchemas.set('AWS::Lambda::Function', {} as ResourceSchema); - return mockSchemas; - }; - - const setupMockSchemas = (schemas: Map) => { - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => schemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - return combinedSchemas; - }; + // Create schemas once at describe level + const testSchemas = combinedSchemas([Schemas.S3Bucket, Schemas.EC2Instance, Schemas.LambdaFunction]); beforeEach(() => { mockComponents.schemaRetriever.getDefault.reset(); + mockComponents.schemaRetriever.getDefault.returns(testSchemas); }); test('should delegate to entity provider when at entity key level', async () => { @@ -126,9 +74,6 @@ describe('ResourceSectionCompletionProvider', () => { data: { Type: 'AWS::' }, }); - const mockSchemas = createMockResourceSchemas(); - setupMockSchemas(mockSchemas); - const typeProvider = resourceProviders.get('Type' as any)!; const mockCompletions = [{ label: 'AWS::S3::Bucket', kind: CompletionItemKind.Class }]; const spy = vi.spyOn(typeProvider, 'getCompletions').mockReturnValue(mockCompletions); @@ -150,9 +95,6 @@ describe('ResourceSectionCompletionProvider', () => { }, }); - const mockSchemas = createMockResourceSchemas(); - setupMockSchemas(mockSchemas); - const propertyProvider = resourceProviders.get('Property' as any)!; const mockCompletions = [ { label: 'BucketName', kind: CompletionItemKind.Property }, @@ -177,9 +119,6 @@ describe('ResourceSectionCompletionProvider', () => { }, }); - const mockSchemas = createMockResourceSchemas(); - setupMockSchemas(mockSchemas); - const propertyProvider = resourceProviders.get('Property' as any)!; const mockCompletions = [ { label: 'Private', kind: CompletionItemKind.EnumMember }, @@ -204,9 +143,6 @@ describe('ResourceSectionCompletionProvider', () => { }, }); - const mockSchemas = createMockResourceSchemas(); - setupMockSchemas(mockSchemas); - const propertyProvider = resourceProviders.get('Property' as any)!; const mockCompletions = [ { label: 'ResourceSignal', kind: CompletionItemKind.Property }, @@ -233,9 +169,6 @@ describe('ResourceSectionCompletionProvider', () => { }, }); - const mockSchemas = createMockResourceSchemas(); - setupMockSchemas(mockSchemas); - const propertyProvider = resourceProviders.get('Property' as any)!; const mockCompletions = [ { label: 'TopicConfigurations', kind: CompletionItemKind.Property }, diff --git a/tst/unit/autocomplete/ResourceStateCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceStateCompletionProvider.test.ts index 9fc98561..57a2cf01 100644 --- a/tst/unit/autocomplete/ResourceStateCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceStateCompletionProvider.test.ts @@ -5,12 +5,13 @@ import { ResourceStateCompletionProvider } from '../../../src/autocomplete/Resou import { DocumentType } from '../../../src/document/Document'; import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { createResourceContext } from '../../utils/MockContext'; -import { createMockComponents } from '../../utils/MockServerComponents'; +import { createMockComponents, createMockSchemaRetriever } from '../../utils/MockServerComponents'; import { combinedSchemas, Schemas } from '../../utils/SchemaUtils'; describe('ResourceStateCompletionProvider', () => { - const mockComponents = createMockComponents(); - const mockSchemas = combinedSchemas(); + const mockComponents = createMockComponents({ + schemaRetriever: createMockSchemaRetriever(defaultSchemas), + }); const provider = new ResourceStateCompletionProvider( mockComponents.resourceStateManager, @@ -29,11 +30,8 @@ describe('ResourceStateCompletionProvider', () => { }; beforeEach(() => { - mockComponents.schemaRetriever.getDefault.reset(); - if (typeof mockComponents.resourceStateManager.getResource.reset === 'function') { - mockComponents.resourceStateManager.getResource.reset(); - } - vi.clearAllMocks(); + mockComponents.schemaRetriever.getDefault.returns(defaultSchemas); + mockComponents.resourceStateManager.getResource.reset(); }); test('should return undefined when resource has no Type', async () => { @@ -67,8 +65,7 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const schemas = combinedSchemas([]); - mockComponents.schemaRetriever.getDefault.returns(schemas); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); const result = await provider.getCompletions(context, mockYamlParams); @@ -84,8 +81,7 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const schemas = combinedSchemas([Schemas.S3Bucket]); - mockComponents.schemaRetriever.getDefault.returns(schemas); + mockComponents.schemaRetriever.getDefault.returns(s3Schemas); const result = await provider.getCompletions(context, mockYamlParams); @@ -101,19 +97,8 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const mockSchema = new ResourceSchema( - JSON.stringify({ - typeName: 'AWS::S3::Bucket', - description: 'Test', - properties: { BucketName: { type: 'string' } }, - primaryIdentifier: [], - additionalProperties: false, - }), - ); - - const schemas = combinedSchemas([]); - schemas.schemas.set('AWS::S3::Bucket', mockSchema); - mockComponents.schemaRetriever.getDefault.returns(schemas); + emptySchemas.schemas.set('AWS::S3::Bucket', s3BucketEmptyPrimaryIdSchema); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); const result = await provider.getCompletions(context, mockYamlParams); @@ -129,8 +114,7 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const schemas = combinedSchemas([Schemas.S3Bucket]); - mockComponents.schemaRetriever.getDefault.returns(schemas); + mockComponents.schemaRetriever.getDefault.returns(s3Schemas); mockComponents.resourceStateManager.getResource.resolves(undefined); const result = await provider.getCompletions(context, mockYamlParams); @@ -147,8 +131,7 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const schemas = combinedSchemas([Schemas.S3Bucket]); - mockComponents.schemaRetriever.getDefault.returns(schemas); + mockComponents.schemaRetriever.getDefault.returns(s3Schemas); mockComponents.resourceStateManager.getResource.resolves({ typeName: 'AWS::S3::Bucket', identifier: 'test', @@ -171,22 +154,8 @@ describe('ResourceStateCompletionProvider', () => { type: DocumentType.YAML, }); - const mockSchema = new ResourceSchema( - JSON.stringify({ - typeName: 'Custom::Type', - description: 'Test', - properties: { - BucketName: { type: 'string' }, - VersioningConfiguration: { type: 'object' }, - }, - primaryIdentifier: ['/properties/BucketName'], - additionalProperties: false, - }), - ); - - const schemas = combinedSchemas([]); - schemas.schemas.set('Custom::Type', mockSchema); - mockComponents.schemaRetriever.getDefault.returns(schemas); + emptySchemas.schemas.set('Custom::Type', customTypeSchema); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); mockComponents.resourceStateManager.getResource.resolves({ typeName: 'Custom::Type', identifier: 'test', @@ -220,22 +189,8 @@ describe('ResourceStateCompletionProvider', () => { type: DocumentType.YAML, }); - const mockSchema = new ResourceSchema( - JSON.stringify({ - typeName: 'Custom::Type', - description: 'Test', - properties: { - BucketName: { type: 'string' }, - VersioningConfiguration: { type: 'object' }, - }, - primaryIdentifier: ['/properties/BucketName'], - additionalProperties: false, - }), - ); - - const schemas = combinedSchemas([]); - schemas.schemas.set('Custom::Type', mockSchema); - mockComponents.schemaRetriever.getDefault.returns(schemas); + emptySchemas.schemas.set('Custom::Type', customTypeSchema); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); mockComponents.resourceStateManager.getResource.resolves({ typeName: 'Custom::Type', identifier: 'test', @@ -270,24 +225,8 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const mockSchema = new ResourceSchema( - JSON.stringify({ - typeName: 'Custom::Type', - description: 'Test', - properties: { - Device: { - type: 'object', - properties: { DeviceName: { type: 'string' } }, - }, - }, - primaryIdentifier: ['/properties/Device/DeviceName'], - additionalProperties: false, - }), - ); - - const schemas = combinedSchemas([]); - schemas.schemas.set('Custom::Type', mockSchema); - mockComponents.schemaRetriever.getDefault.returns(schemas); + emptySchemas.schemas.set('Custom::Type', customTypeWithNestedIdSchema); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); const result = await provider.getCompletions(context, mockYamlParams); @@ -312,22 +251,8 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const mockSchema = new ResourceSchema( - JSON.stringify({ - typeName: 'Custom::Type', - description: 'Test', - properties: { - Id1: { type: 'string' }, - Id2: { type: 'string' }, - }, - primaryIdentifier: ['/properties/Id1', '/properties/Id2'], - additionalProperties: false, - }), - ); - - const schemas = combinedSchemas([]); - schemas.schemas.set('Custom::Type', mockSchema); - mockComponents.schemaRetriever.getDefault.returns(schemas); + emptySchemas.schemas.set('Custom::Type', customTypeWithMultipleIdsSchema); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); const result = await provider.getCompletions(context, mockYamlParams); @@ -336,7 +261,7 @@ describe('ResourceStateCompletionProvider', () => { }); test('should remove readonly and already defined properties - JSON template', async () => { - mockComponents.schemaRetriever.getDefault.returns(mockSchemas); + mockComponents.schemaRetriever.getDefault.returns(defaultSchemas); mockComponents.documentManager.getLine.returns('"",'); const context = createResourceContext('MyResource', { text: '', @@ -375,7 +300,7 @@ describe('ResourceStateCompletionProvider', () => { }); test('should remove readonly and already defined properties - YAML template', async () => { - mockComponents.schemaRetriever.getDefault.returns(mockSchemas); + mockComponents.schemaRetriever.getDefault.returns(defaultSchemas); mockComponents.documentManager.getLine.returns('"",'); const context = createResourceContext('MyResource', { text: '', @@ -422,24 +347,8 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const mockSchema = new ResourceSchema( - JSON.stringify({ - typeName: 'Custom::Type', - description: 'Test', - properties: { - Device: { - type: 'object', - properties: { DeviceName: { type: 'string' } }, - }, - }, - primaryIdentifier: ['/properties/Device/DeviceName'], - additionalProperties: false, - }), - ); - - const schemas = combinedSchemas([]); - schemas.schemas.set('Custom::Type', mockSchema); - mockComponents.schemaRetriever.getDefault.returns(schemas); + emptySchemas.schemas.set('Custom::Type', customTypeWithNestedIdSchema); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); const result = await provider.getCompletions(context, mockYamlParams); @@ -455,24 +364,8 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const mockSchema = new ResourceSchema( - JSON.stringify({ - typeName: 'Custom::Type', - description: 'Test', - properties: { - Device: { - type: 'object', - properties: { DeviceName: { type: 'string' } }, - }, - }, - primaryIdentifier: ['/properties/Device/DeviceName'], - additionalProperties: false, - }), - ); - - const schemas = combinedSchemas([]); - schemas.schemas.set('Custom::Type', mockSchema); - mockComponents.schemaRetriever.getDefault.returns(schemas); + emptySchemas.schemas.set('Custom::Type', customTypeWithNestedIdSchema); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); const result = await provider.getCompletions(context, mockYamlParams); @@ -496,19 +389,8 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const mockSchema = new ResourceSchema( - JSON.stringify({ - typeName: 'Custom::Type', - description: 'Test', - properties: { DeviceName: { type: 'string' } }, - primaryIdentifier: ['DeviceName'], - additionalProperties: false, - }), - ); - - const schemas = combinedSchemas([]); - schemas.schemas.set('Custom::Type', mockSchema); - mockComponents.schemaRetriever.getDefault.returns(schemas); + emptySchemas.schemas.set('Custom::Type', customTypeWithNoPrefixSchema); + mockComponents.schemaRetriever.getDefault.returns(emptySchemas); const result = await provider.getCompletions(context, mockYamlParams); @@ -525,11 +407,75 @@ describe('ResourceStateCompletionProvider', () => { }, }); - const schemas = combinedSchemas([Schemas.S3Bucket]); - mockComponents.schemaRetriever.getDefault.returns(schemas); + mockComponents.schemaRetriever.getDefault.returns(s3Schemas); const result = await provider.getCompletions(context, mockYamlParams); expect(result.length).toBe(0); }); }); + +const defaultSchemas = combinedSchemas(); +const s3Schemas = combinedSchemas([Schemas.S3Bucket]); +const emptySchemas = combinedSchemas([]); + +const customTypeSchema = new ResourceSchema( + JSON.stringify({ + typeName: 'Custom::Type', + description: 'Test', + properties: { + BucketName: { type: 'string' }, + VersioningConfiguration: { type: 'object' }, + }, + primaryIdentifier: ['/properties/BucketName'], + additionalProperties: false, + }), +); + +const customTypeWithNestedIdSchema = new ResourceSchema( + JSON.stringify({ + typeName: 'Custom::Type', + description: 'Test', + properties: { + Device: { + type: 'object', + properties: { DeviceName: { type: 'string' } }, + }, + }, + primaryIdentifier: ['/properties/Device/DeviceName'], + additionalProperties: false, + }), +); + +const customTypeWithMultipleIdsSchema = new ResourceSchema( + JSON.stringify({ + typeName: 'Custom::Type', + description: 'Test', + properties: { + Id1: { type: 'string' }, + Id2: { type: 'string' }, + }, + primaryIdentifier: ['/properties/Id1', '/properties/Id2'], + additionalProperties: false, + }), +); + +const customTypeWithNoPrefixSchema = new ResourceSchema( + JSON.stringify({ + typeName: 'Custom::Type', + description: 'Test', + properties: { DeviceName: { type: 'string' } }, + primaryIdentifier: ['DeviceName'], + additionalProperties: false, + }), +); + +const s3BucketEmptyPrimaryIdSchema = new ResourceSchema( + JSON.stringify({ + typeName: 'AWS::S3::Bucket', + description: 'Test', + properties: { BucketName: { type: 'string' } }, + primaryIdentifier: [], + additionalProperties: false, + }), +); diff --git a/tst/unit/autocomplete/ResourceTypeCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceTypeCompletionProvider.test.ts index cac9ac76..44c5f526 100644 --- a/tst/unit/autocomplete/ResourceTypeCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceTypeCompletionProvider.test.ts @@ -1,11 +1,11 @@ import { describe, expect, test, beforeEach } from 'vitest'; import { CompletionParams, CompletionItemKind } from 'vscode-languageserver'; import { ResourceTypeCompletionProvider } from '../../../src/autocomplete/ResourceTypeCompletionProvider'; -import { CombinedSchemas } from '../../../src/schema/CombinedSchemas'; import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { ExtensionName } from '../../../src/utils/ExtensionConfig'; import { createResourceContext } from '../../utils/MockContext'; import { createMockComponents } from '../../utils/MockServerComponents'; +import { combinedSchemas, combineSchema, Schemas } from '../../utils/SchemaUtils'; describe('ResourceTypeCompletionProvider', () => { const mockComponents = createMockComponents(); @@ -16,23 +16,14 @@ describe('ResourceTypeCompletionProvider', () => { position: { line: 0, character: 0 }, }; - const createMockResourceSchemas = () => { - const mockSchemas = new Map(); - mockSchemas.set('AWS::EC2::Instance', {} as ResourceSchema); - mockSchemas.set('AWS::S3::Bucket', {} as ResourceSchema); - mockSchemas.set('AWS::S3::BucketPolicy', {} as ResourceSchema); - mockSchemas.set('AWS::Lambda::Function', {} as ResourceSchema); - return mockSchemas; - }; - - const setupMockSchemas = (schemas: Map) => { - const combinedSchemas = new CombinedSchemas(); - Object.defineProperty(combinedSchemas, 'schemas', { - get: () => schemas, - }); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas); - return combinedSchemas; - }; + const s3BucketSchema = new ResourceSchema(Schemas.S3Bucket.contents); + const fourSchemas = combinedSchemas([ + Schemas.S3Bucket, + Schemas.EC2Instance, + Schemas.LambdaFunction, + combineSchema(s3BucketSchema, 'AWS::S3::BucketPolicy', {}), + ]); + const twoSchemas = combinedSchemas([Schemas.S3Bucket, Schemas.EC2Instance]); beforeEach(() => { mockComponents.schemaRetriever.getDefault.reset(); @@ -44,8 +35,7 @@ describe('ResourceTypeCompletionProvider', () => { propertyPath: ['Resources', 'MyResource', 'Type'], data: { Type: 'AWS::' }, }); - const mockSchemas = createMockResourceSchemas(); - setupMockSchemas(mockSchemas); + mockComponents.schemaRetriever.getDefault.returns(fourSchemas); const result = provider.getCompletions(mockContext, mockParams); @@ -81,8 +71,7 @@ describe('ResourceTypeCompletionProvider', () => { Type: 'AWS::', }, }); - const mockSchemas = createMockResourceSchemas(); - setupMockSchemas(mockSchemas); + mockComponents.schemaRetriever.getDefault.returns(fourSchemas); const result = provider.getCompletions(mockContext, mockParams); @@ -105,11 +94,7 @@ describe('ResourceTypeCompletionProvider', () => { Type: 'AWS::', }, }); - - const mockSchemas = new Map(); - mockSchemas.set('AWS::EC2::Instance', {} as ResourceSchema); - mockSchemas.set('AWS::S3::Bucket', {} as ResourceSchema); - setupMockSchemas(mockSchemas); + mockComponents.schemaRetriever.getDefault.returns(twoSchemas); const result = provider.getCompletions(mockContext, mockParams); diff --git a/tst/unit/handlers/StackHandler.test.ts b/tst/unit/handlers/StackHandler.test.ts index cf5ace10..5b53285c 100644 --- a/tst/unit/handlers/StackHandler.test.ts +++ b/tst/unit/handlers/StackHandler.test.ts @@ -102,10 +102,13 @@ describe('StackActionHandler', () => { let getEntityMapSpy: any; const mockToken = {} as CancellationToken; + const testSchemas = combinedSchemas(); + beforeEach(() => { syntaxTreeManager = createMockSyntaxTreeManager(); mockComponents = createMockComponents({ syntaxTreeManager }); getEntityMapSpy = vi.mocked(SectionContextBuilder.getEntityMap); + mockComponents.schemaRetriever.getDefault.returns(testSchemas); mockComponents.validationWorkflowService.start.reset(); mockComponents.validationWorkflowService.getStatus.reset(); mockComponents.deploymentWorkflowService.start.reset(); @@ -615,8 +618,6 @@ describe('StackActionHandler', () => { syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree); getEntityMapSpy.mockReturnValue(resourcesMap); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas()); - const handler = getTemplateResourcesHandler(mockComponents); const result = handler(templateUri, mockToken) as GetTemplateResourcesResult; @@ -645,8 +646,6 @@ describe('StackActionHandler', () => { syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree); getEntityMapSpy.mockReturnValue(resourcesMap); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas()); - const handler = getTemplateResourcesHandler(mockComponents); const result = handler(templateUri, mockToken) as GetTemplateResourcesResult; @@ -677,8 +676,6 @@ describe('StackActionHandler', () => { syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree); getEntityMapSpy.mockReturnValue(resourcesMap); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas()); - const handler = getTemplateResourcesHandler(mockComponents); const result = handler(templateUri, mockToken) as GetTemplateResourcesResult; @@ -751,8 +748,6 @@ describe('StackActionHandler', () => { syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree); getEntityMapSpy.mockReturnValue(resourcesMap); - mockComponents.schemaRetriever.getDefault.returns(combinedSchemas()); - const handler = getTemplateResourcesHandler(mockComponents); const result = handler(templateUri, mockToken) as GetTemplateResourcesResult; diff --git a/tst/unit/relatedResources/RelatedResourcesSnippetProvider.test.ts b/tst/unit/relatedResources/RelatedResourcesSnippetProvider.test.ts index 97735834..92c07732 100644 --- a/tst/unit/relatedResources/RelatedResourcesSnippetProvider.test.ts +++ b/tst/unit/relatedResources/RelatedResourcesSnippetProvider.test.ts @@ -19,34 +19,30 @@ vi.mock('../../../src/context/SectionContextBuilder', () => ({ })); describe('RelatedResourcesSnippetProvider', () => { + const defaultSchemas = combinedSchemas(); + const syntaxTreeManager = createMockSyntaxTreeManager(); const documentManager = createMockDocumentManager(); - const schemaRetriever = createMockSchemaRetriever(); + const schemaRetriever = createMockSchemaRetriever(defaultSchemas); const relationshipSchemaService = createMockRelationshipSchemaService(); - let mockComponents: ReturnType; - let provider: RelatedResourcesSnippetProvider; - let mockGetEntityMap: any; + const mockComponents = createMockComponents({ + syntaxTreeManager, + documentManager, + schemaRetriever, + relationshipSchemaService, + }); + const provider = new RelatedResourcesSnippetProvider( + mockComponents.documentManager, + mockComponents.syntaxTreeManager, + mockComponents.schemaRetriever, + ); + const mockGetEntityMap = vi.mocked(getEntityMap) as any; beforeEach(() => { - vi.clearAllMocks(); + mockGetEntityMap.mockReset(); syntaxTreeManager.getSyntaxTree.reset(); - syntaxTreeManager.add.reset(); - syntaxTreeManager.deleteSyntaxTree.reset(); documentManager.get.reset(); - schemaRetriever.getDefault.reset(); - - mockComponents = createMockComponents({ - syntaxTreeManager, - documentManager, - schemaRetriever, - relationshipSchemaService, - }); - provider = new RelatedResourcesSnippetProvider( - mockComponents.documentManager, - mockComponents.syntaxTreeManager, - mockComponents.schemaRetriever, - ); - mockGetEntityMap = vi.mocked(getEntityMap); + schemaRetriever.getDefault.returns(defaultSchemas); }); describe('insertRelatedResources', () => { @@ -66,7 +62,6 @@ describe('RelatedResourcesSnippetProvider', () => { documentManager.get.withArgs(templateUri).returns(document); syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(undefined); - schemaRetriever.getDefault.returns(combinedSchemas()); const result = provider.insertRelatedResources(templateUri, ['AWS::Lambda::Function'], 'AWS::S3::Bucket'); @@ -109,7 +104,6 @@ Resources: documentManager.get.withArgs(templateUri).returns(document); syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree as any); mockGetEntityMap.mockReturnValue(new Map([['MyBucket', { entity: { Type: 'AWS::S3::Bucket' } }]])); - schemaRetriever.getDefault.returns(combinedSchemas()); const result = provider.insertRelatedResources(templateUri, ['AWS::Lambda::Function'], 'AWS::S3::Bucket'); @@ -128,7 +122,6 @@ Resources: documentManager.get.withArgs(templateUri).returns(document); syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(undefined); - schemaRetriever.getDefault.returns(combinedSchemas()); const result = provider.insertRelatedResources(templateUri, ['AWS::Lambda::Function'], 'AWS::S3::Bucket'); @@ -167,7 +160,6 @@ Resources: documentManager.get.withArgs(templateUri).returns(document); syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree as any); mockGetEntityMap.mockReturnValue(new Map([['MyBucket', { entity: { Type: 'AWS::S3::Bucket' } }]])); - schemaRetriever.getDefault.returns(combinedSchemas()); const result = provider.insertRelatedResources(templateUri, ['AWS::Lambda::Function'], 'AWS::S3::Bucket'); @@ -202,7 +194,6 @@ Resources: documentManager.get.withArgs(templateUri).returns(document); syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree as any); mockGetEntityMap.mockReturnValue(new Map([['MyBucket', { entity: { Type: 'AWS::S3::Bucket' } }]])); - schemaRetriever.getDefault.returns(combinedSchemas()); const result = provider.insertRelatedResources( templateUri, @@ -293,7 +284,6 @@ Resources: ['LambdaFunctionRelatedToS3Bucket', { entity: { Type: 'AWS::Lambda::Function' } }], ]), ); - schemaRetriever.getDefault.returns(combinedSchemas()); const result = provider.insertRelatedResources(templateUri, ['AWS::Lambda::Function'], 'AWS::S3::Bucket'); @@ -328,7 +318,6 @@ Resources: documentManager.get.withArgs(templateUri).returns(document); syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree as any); mockGetEntityMap.mockReturnValue(new Map([['MyBucket', { entity: { Type: 'AWS::S3::Bucket' } }]])); - schemaRetriever.getDefault.returns(combinedSchemas()); const result = provider.insertRelatedResources(templateUri, ['AWS::Lambda::Function'], 'AWS::S3::Bucket'); diff --git a/tst/unit/resourceState/ResourceStateImporter.test.ts b/tst/unit/resourceState/ResourceStateImporter.test.ts index 1fc95df4..790235b0 100644 --- a/tst/unit/resourceState/ResourceStateImporter.test.ts +++ b/tst/unit/resourceState/ResourceStateImporter.test.ts @@ -19,7 +19,7 @@ describe('ResourceStateImporter', () => { let mockResourceStateManager: any; let documentManager: DocumentManager; let syntaxTreeManager: SyntaxTreeManager; - let schemaRetriever: ReturnType; + const schemaRetriever = createMockSchemaRetriever(combinedSchemas()); const mockStackManagementInfoProvider = createMockStackManagementInfoProvider(); let importer: ResourceStateImporter; @@ -37,7 +37,6 @@ describe('ResourceStateImporter', () => { detectIndentation: false, }; - schemaRetriever = createMockSchemaRetriever(combinedSchemas()); mockResourceStateManager = { getResource: vi.fn(), listResources: vi.fn(), diff --git a/tst/unit/services/cfnLint/PyodideWorkerManager.test.ts b/tst/unit/services/cfnLint/PyodideWorkerManager.test.ts index e619b827..b149488a 100644 --- a/tst/unit/services/cfnLint/PyodideWorkerManager.test.ts +++ b/tst/unit/services/cfnLint/PyodideWorkerManager.test.ts @@ -5,6 +5,7 @@ import { describe, expect, beforeEach, vi, test, Mock } from 'vitest'; import { CloudFormationFileType } from '../../../../src/document/Document'; import { PyodideWorkerManager } from '../../../../src/services/cfnLint/PyodideWorkerManager'; import { CfnLintSettings } from '../../../../src/settings/Settings'; +import * as RetryModule from '../../../../src/utils/Retry'; import { mockLogger } from '../../../utils/MockServerComponents'; // Mock Worker class @@ -719,223 +720,44 @@ describe('PyodideWorkerManager', () => { expect(mockLogging.warn.callCount).toBe(1); // 1 retry warning }); - test('should use exponential backoff delays', async () => { - // Create a worker manager with specific retry settings - const retryWorkerManager = new PyodideWorkerManager( - { - maxRetries: 2, - initialDelayMs: 20, - maxDelayMs: 100, - backoffMultiplier: 3, - totalTimeoutMs: 10000, // Large timeout so it doesn't interfere - }, - createDefaultCfnLintSettings(), + test('should pass correct config to retryWithExponentialBackoff', async () => { + const retrySpy = vi.spyOn(RetryModule, 'retryWithExponentialBackoff'); - mockLogging, - ); - - const startTime = Date.now(); - const attemptTimes: number[] = []; + const retryConfig = { + maxRetries: 5, + initialDelayMs: 123, + maxDelayMs: 456, + backoffMultiplier: 3, + totalTimeoutMs: 9999, + }; - workerConstructor.mockImplementation(() => { - attemptTimes.push(Date.now() - startTime); - throw new Error('Worker creation failed'); - }); - - // Expect initialization to fail - await expect(retryWorkerManager.initialize()).rejects.toThrow(); - - // Verify exponential backoff timing with more lenient assertions for CI - expect(attemptTimes.length).toBe(3); // Initial + 2 retries - - // Calculate actual delays between attempts - const delays = [ - attemptTimes[1] - attemptTimes[0], // First delay - attemptTimes[2] - attemptTimes[1], // Second delay - ]; - - // First attempt should be immediate - expect(attemptTimes[0]).toBeLessThan(20); - - // First delay should be close to initialDelayMs (20ms), but allow variance for CI - expect(delays[0]).toBeGreaterThanOrEqual(10); - expect(delays[0]).toBeLessThan(50); - - // Second delay should be approximately backoffMultiplier (3) times the first - // But allow more variance for CI environments - const ratio = delays[1] / delays[0]; - expect(ratio).toBeGreaterThanOrEqual(2); - expect(ratio).toBeLessThanOrEqual(5); // More lenient upper bound - }); - - test('should respect maxDelayMs cap', async () => { - // Create a worker manager with low maxDelayMs const retryWorkerManager = new PyodideWorkerManager( - { - maxRetries: 3, - initialDelayMs: 50, - maxDelayMs: 80, // Cap at 80ms - backoffMultiplier: 10, // Would normally create very long delays - totalTimeoutMs: 10000, // Large timeout so it doesn't interfere - }, + retryConfig, createDefaultCfnLintSettings(), - mockLogging, ); - const startTime = Date.now(); - const attemptTimes: number[] = []; - workerConstructor.mockImplementation(() => { - attemptTimes.push(Date.now() - startTime); - throw new Error('Worker creation failed'); + throw new Error('fail'); }); - // Expect initialization to fail await expect(retryWorkerManager.initialize()).rejects.toThrow(); - // Verify delays are capped at maxDelayMs - expect(attemptTimes.length).toBe(4); // Initial + 3 retries - - // Calculate actual delays between attempts - const delays = [ - attemptTimes[1] - attemptTimes[0], // First delay - attemptTimes[2] - attemptTimes[1], // Second delay - attemptTimes[3] - attemptTimes[2], // Third delay - ]; - - // First delay should be close to initialDelayMs (50ms), but allow variance for CI - expect(delays[0]).toBeGreaterThanOrEqual(40); - expect(delays[0]).toBeLessThan(80); - - // Subsequent delays should be capped at maxDelayMs (80ms), not exponentially growing - // Allow for reasonable timing variations but verify the cap is working - expect(delays[1]).toBeGreaterThanOrEqual(60); - expect(delays[1]).toBeLessThanOrEqual(120); - - expect(delays[2]).toBeGreaterThanOrEqual(60); - expect(delays[2]).toBeLessThanOrEqual(120); - }); - - test('should apply jitter to prevent synchronized retry storms', async () => { - // Create a worker manager with jitter enabled (jitter is hardcoded to 0.1 in implementation) - const retryWorkerManager = new PyodideWorkerManager( - { - maxRetries: 2, - initialDelayMs: 100, - maxDelayMs: 1000, - backoffMultiplier: 2, - totalTimeoutMs: 10000, // Large timeout so it doesn't interfere - }, - createDefaultCfnLintSettings(), - - mockLogging, - ); - - const attemptTimes: number[] = []; - const startTime = Date.now(); - - workerConstructor.mockImplementation(() => { - attemptTimes.push(Date.now() - startTime); - throw new Error('Worker creation failed'); - }); - - // Expect initialization to fail - await expect(retryWorkerManager.initialize()).rejects.toThrow(); - - // Verify that delays have some variance due to jitter (hardcoded 10% jitter in implementation) - expect(attemptTimes.length).toBe(3); // Initial + 2 retries - - // Calculate actual delays between attempts - const delays = [ - attemptTimes[1] - attemptTimes[0], // First delay - attemptTimes[2] - attemptTimes[1], // Second delay - ]; - - // With jitter, the delays should not be exactly the expected values - // First retry should be around 100ms ± 10ms jitter, but allow more variance for CI - expect(delays[0]).toBeGreaterThan(80); - expect(delays[0]).toBeLessThan(140); - - // Second retry should be around 200ms ± 20ms jitter, but allow more variance for CI - expect(delays[1]).toBeGreaterThan(160); - expect(delays[1]).toBeLessThan(280); - }); - - test('should respect total timeout to prevent excessive retry durations', async () => { - // Create a worker manager with settings that would normally take a long time - // The implementation uses totalTimeoutMs = maxDelayMs * (maxRetries + 1) - // With maxDelayMs=50 and maxRetries=10, totalTimeout = 50 * 11 = 550ms - // But the actual delays grow exponentially, so it should timeout before reaching max retries - const retryWorkerManager = new PyodideWorkerManager( - { - maxRetries: 10, // High retry count - initialDelayMs: 100, // Start with higher delay - maxDelayMs: 200, // This means totalTimeout = 200 * 11 = 2200ms - backoffMultiplier: 2, - totalTimeoutMs: 2200, // Explicit timeout - }, - createDefaultCfnLintSettings(), - - mockLogging, - ); - - const startTime = Date.now(); - let attemptCount = 0; - - workerConstructor.mockImplementation(() => { - attemptCount++; - throw new Error('Worker creation failed'); - }); - - // Expect initialization to fail - await expect(retryWorkerManager.initialize()).rejects.toThrow(); - - const totalTime = Date.now() - startTime; - - // The total timeout should prevent it from running the full duration - // With exponential backoff (100, 200, 200, 200...), it should timeout before all retries - expect(totalTime).toBeLessThan(3000); // Should be around 2200ms + some buffer - - // It might still reach max retries depending on timing, so let's just verify it ran - expect(attemptCount).toBeGreaterThan(1); // Should have made multiple attempts - }); - - test('should use explicit totalTimeoutMs when provided', async () => { - // Create a worker manager with explicit totalTimeoutMs - const retryWorkerManager = new PyodideWorkerManager( - { - maxRetries: 10, // High retry count that would normally take a long time - initialDelayMs: 50, - maxDelayMs: 200, - backoffMultiplier: 2, - totalTimeoutMs: 300, // Explicit timeout of 300ms - }, - createDefaultCfnLintSettings(), - - mockLogging, + expect(retrySpy).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ + maxRetries: 5, + initialDelayMs: 123, + maxDelayMs: 456, + backoffMultiplier: 3, + totalTimeoutMs: 9999, + jitterFactor: 0.1, + operationName: 'Pyodide initialization', + }), + expect.anything(), ); - const startTime = Date.now(); - let attemptCount = 0; - - workerConstructor.mockImplementation(() => { - attemptCount++; - throw new Error('Worker creation failed'); - }); - - // Expect initialization to fail with timeout error - await expect(retryWorkerManager.initialize()).rejects.toThrow(/Pyodide initialization timed out after/); - - const totalTime = Date.now() - startTime; - - // Should respect the explicit totalTimeoutMs (300ms) rather than calculated timeout - expect(totalTime).toBeGreaterThanOrEqual(290); // Should run for at least the timeout duration - expect(totalTime).toBeLessThan(400); // Should not run much longer than timeout + buffer - - // Should have made some attempts but not all 10 retries - expect(attemptCount).toBeGreaterThan(1); - expect(attemptCount).toBeLessThan(11); // Should timeout before reaching max retries + retrySpy.mockRestore(); }); }); diff --git a/tst/unit/tools/wheel-download.test.ts b/tst/unit/tools/wheel-download.test.ts deleted file mode 100644 index 27680b41..00000000 --- a/tst/unit/tools/wheel-download.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { execSync } from 'child_process'; -import { existsSync, mkdirSync, readdirSync, rmSync } from 'fs'; -import { join } from 'path'; -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; - -describe('wheel download utility', () => { - const testWheelsDir = join(process.cwd(), 'node_modules', '.cache', 'test-wheels'); - - beforeEach(() => { - if (existsSync(testWheelsDir)) { - rmSync(testWheelsDir, { recursive: true, force: true }); - } - mkdirSync(testWheelsDir, { recursive: true }); - }); - - afterEach(() => { - if (existsSync(testWheelsDir)) { - rmSync(testWheelsDir, { recursive: true, force: true }); - } - }); - - it('should download cfn-lint and dependencies', () => { - execSync(`python3 -m pip download --dest ${testWheelsDir} --only-binary=:all: cfn-lint`, { - stdio: 'pipe', - }); - - const wheels = readdirSync(testWheelsDir).filter((file) => file.endsWith('.whl')); - expect(wheels.length).toBeGreaterThan(0); - - const cfnLintWheels = wheels.filter((wheel) => wheel.startsWith('cfn_lint-')); - expect(cfnLintWheels.length).toBe(1); - }); - - it('should identify Pyodide packages for exclusion', () => { - execSync(`python3 -m pip download --dest ${testWheelsDir} --only-binary=:all: cfn-lint`, { - stdio: 'pipe', - }); - - const wheels = readdirSync(testWheelsDir).filter((file) => file.endsWith('.whl')); - const pyodidePackages = ['pyyaml', 'regex', 'rpds_py', 'pydantic', 'pydantic_core']; - - const foundPyodidePackages = wheels.filter((wheel) => - pyodidePackages.some((pkg) => wheel.startsWith(pkg) || wheel.startsWith(pkg.replace('_', '-'))), - ); - - expect(foundPyodidePackages.length).toBeGreaterThan(0); - }); -}); From ddf2975957b3bae19bc48f158d04d68e84621b5c Mon Sep 17 00:00:00 2001 From: Satyaki Ghosh Date: Tue, 9 Dec 2025 14:55:22 -0500 Subject: [PATCH 2/2] Update method names --- .../autocomplete/ResourceEntityCompletionProvider.test.ts | 6 +++--- .../ResourceSectionCompletionProvider.test.ts | 1 - .../autocomplete/ResourceTypeCompletionProvider.test.ts | 4 ++-- tst/unit/hover/ResourceSectionHoverProvider.test.ts | 8 ++++---- tst/utils/SchemaUtils.ts | 2 +- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts index 32d8b0f2..ad82e856 100644 --- a/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceEntityCompletionProvider.test.ts @@ -7,7 +7,7 @@ import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { ExtensionName } from '../../../src/utils/ExtensionConfig'; import { createForEachResourceContext, createResourceContext } from '../../utils/MockContext'; import { createMockComponents } from '../../utils/MockServerComponents'; -import { combinedSchemas, combineSchema, Schemas } from '../../utils/SchemaUtils'; +import { combinedSchemas, createSchemaFrom, Schemas } from '../../utils/SchemaUtils'; describe('ResourceEntityCompletionProvider', () => { const mockComponents = createMockComponents(); @@ -23,10 +23,10 @@ describe('ResourceEntityCompletionProvider', () => { // Create schemas once at describe level const ec2Schema = new ResourceSchema(Schemas.EC2Instance.contents); - const ec2WithRequiredProps = combineSchema(ec2Schema, 'AWS::EC2::Instance', { + const ec2WithRequiredProps = createSchemaFrom(ec2Schema, 'AWS::EC2::Instance', { required: ['InstanceType', 'ImageId'], }); - const ec2WithNoRequiredProps = combineSchema(ec2Schema, 'AWS::EC2::Instance', { + const ec2WithNoRequiredProps = createSchemaFrom(ec2Schema, 'AWS::EC2::Instance', { required: [], }); const schemasWithRequired = combinedSchemas([ec2WithRequiredProps]); diff --git a/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts index 5ff77863..76a203ae 100644 --- a/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceSectionCompletionProvider.test.ts @@ -44,7 +44,6 @@ describe('ResourceSectionCompletionProvider', () => { const testSchemas = combinedSchemas([Schemas.S3Bucket, Schemas.EC2Instance, Schemas.LambdaFunction]); beforeEach(() => { - mockComponents.schemaRetriever.getDefault.reset(); mockComponents.schemaRetriever.getDefault.returns(testSchemas); }); diff --git a/tst/unit/autocomplete/ResourceTypeCompletionProvider.test.ts b/tst/unit/autocomplete/ResourceTypeCompletionProvider.test.ts index 44c5f526..7968ebfa 100644 --- a/tst/unit/autocomplete/ResourceTypeCompletionProvider.test.ts +++ b/tst/unit/autocomplete/ResourceTypeCompletionProvider.test.ts @@ -5,7 +5,7 @@ import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { ExtensionName } from '../../../src/utils/ExtensionConfig'; import { createResourceContext } from '../../utils/MockContext'; import { createMockComponents } from '../../utils/MockServerComponents'; -import { combinedSchemas, combineSchema, Schemas } from '../../utils/SchemaUtils'; +import { combinedSchemas, createSchemaFrom, Schemas } from '../../utils/SchemaUtils'; describe('ResourceTypeCompletionProvider', () => { const mockComponents = createMockComponents(); @@ -21,7 +21,7 @@ describe('ResourceTypeCompletionProvider', () => { Schemas.S3Bucket, Schemas.EC2Instance, Schemas.LambdaFunction, - combineSchema(s3BucketSchema, 'AWS::S3::BucketPolicy', {}), + createSchemaFrom(s3BucketSchema, 'AWS::S3::BucketPolicy', {}), ]); const twoSchemas = combinedSchemas([Schemas.S3Bucket, Schemas.EC2Instance]); diff --git a/tst/unit/hover/ResourceSectionHoverProvider.test.ts b/tst/unit/hover/ResourceSectionHoverProvider.test.ts index 2081e9b3..140b54df 100644 --- a/tst/unit/hover/ResourceSectionHoverProvider.test.ts +++ b/tst/unit/hover/ResourceSectionHoverProvider.test.ts @@ -19,7 +19,7 @@ import { ResourceSectionHoverProvider } from '../../../src/hover/ResourceSection import { ResourceSchema } from '../../../src/schema/ResourceSchema'; import { createMockContext, createResourceContext } from '../../utils/MockContext'; import { createMockSchemaRetriever } from '../../utils/MockServerComponents'; -import { combinedSchemas, combineSchema, Schemas } from '../../utils/SchemaUtils'; +import { combinedSchemas, createSchemaFrom, Schemas } from '../../utils/SchemaUtils'; import { docPosition, Templates } from '../../utils/TemplateUtils'; describe('ResourceSectionHoverProvider', () => { @@ -28,13 +28,13 @@ describe('ResourceSectionHoverProvider', () => { const mockCombinedSchemas = combinedSchemas([ Schemas.S3Bucket, Schemas.EC2Instance, - combineSchema(s3BucketSchema, 'AWS::S3::BucketNameRequired', { + createSchemaFrom(s3BucketSchema, 'AWS::S3::BucketNameRequired', { required: ['BucketName'], }), - combineSchema(s3BucketSchema, 'AWS::S3::BucketNameEmptyRequired', { + createSchemaFrom(s3BucketSchema, 'AWS::S3::BucketNameEmptyRequired', { required: [], }), - combineSchema(s3BucketSchema, 'AWS::S3::BucketNameBadRef', { + createSchemaFrom(s3BucketSchema, 'AWS::S3::BucketNameBadRef', { properties: { UnresolvableProperty: { $ref: '#/definitions/NonExistentDefinition', diff --git a/tst/utils/SchemaUtils.ts b/tst/utils/SchemaUtils.ts index 353130d5..6d9111be 100644 --- a/tst/utils/SchemaUtils.ts +++ b/tst/utils/SchemaUtils.ts @@ -187,7 +187,7 @@ export function combinedSchemas( ); } -export function combineSchema(schema: ResourceSchema, newName: string, changes: any): typeof Schemas.S3Bucket { +export function createSchemaFrom(schema: ResourceSchema, newName: string, changes: any): typeof Schemas.S3Bucket { return { fileName: `${newName.toLowerCase().split('::').join('-')}.json`, contents: JSON.stringify({