Path: blob/main/src/vs/workbench/contrib/languageStatus/test/common/languageStatusDedupe.test.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { dispose, IDisposable } from '../../../../../base/common/lifecycle.js';7import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';89/**10* Tests for the dedicated entry deduplication logic used in LanguageStatus._update.11*12* The pattern under test mirrors the dedicated-entry loop in languageStatus.ts:13* when building the new dedicated entries map, we must check both14* `newDedicatedEntries` (for duplicates within the current update) and15* `_dedicatedEntries` (for entries from the previous update) to avoid16* orphaning entry accessors and leaking their event listeners.17*/1819interface MockAccessor extends IDisposable {20id: string;21updateCount: number;22disposed: boolean;23update(): void;24}2526function createMockAccessor(id: string): MockAccessor {27return {28id,29updateCount: 0,30disposed: false,31update() { this.updateCount++; },32dispose() { this.disposed = true; }33};34}3536suite('LanguageStatus - Dedicated Entry Deduplication', () => {3738ensureNoDisposablesAreLeakedInTestSuite();3940/**41* Simulates the dedicated-entry update loop from LanguageStatus._update,42* using the FIXED logic that checks newDedicatedEntries before creating.43*/44function runDedicatedEntryUpdate(45modelDedicatedIds: string[],46existingEntries: Map<string, MockAccessor>,47createEntry: (id: string) => MockAccessor48): Map<string, MockAccessor> {49const newDedicatedEntries = new Map<string, MockAccessor>();50for (const id of modelDedicatedIds) {51let entry = newDedicatedEntries.get(id) ?? existingEntries.get(id);52if (!entry) {53entry = createEntry(id);54} else {55entry.update();56existingEntries.delete(id);57}58newDedicatedEntries.set(id, entry);59}60dispose(existingEntries.values());61return newDedicatedEntries;62}6364/**65* Simulates the OLD (buggy) dedicated-entry update loop that only checks66* existingEntries, not newDedicatedEntries.67*/68function runDedicatedEntryUpdateBuggy(69modelDedicatedIds: string[],70existingEntries: Map<string, MockAccessor>,71createEntry: (id: string) => MockAccessor72): Map<string, MockAccessor> {73const newDedicatedEntries = new Map<string, MockAccessor>();74for (const id of modelDedicatedIds) {75let entry = existingEntries.get(id);76if (!entry) {77entry = createEntry(id);78} else {79entry.update();80existingEntries.delete(id);81}82newDedicatedEntries.set(id, entry);83}84dispose(existingEntries.values());85return newDedicatedEntries;86}8788test('reuses existing entry from previous update', () => {89const existing = new Map<string, MockAccessor>();90const oldEntry = createMockAccessor('status-A');91existing.set('status-A', oldEntry);9293const result = runDedicatedEntryUpdate(['status-A'], existing, createMockAccessor);9495assert.strictEqual(result.get('status-A'), oldEntry, 'should reuse the existing entry');96assert.strictEqual(oldEntry.updateCount, 1, 'should have updated the entry');97assert.strictEqual(oldEntry.disposed, false, 'should not dispose reused entry');98});99100test('creates new entry when none exists', () => {101const existing = new Map<string, MockAccessor>();102103const result = runDedicatedEntryUpdate(['status-A'], existing, createMockAccessor);104105const entry = result.get('status-A')!;106assert.ok(entry, 'should create a new entry');107assert.strictEqual(entry.updateCount, 0, 'should not have called update on new entry');108assert.strictEqual(entry.disposed, false, 'new entry should not be disposed');109});110111test('disposes entries no longer in model', () => {112const existing = new Map<string, MockAccessor>();113const oldEntry = createMockAccessor('status-A');114existing.set('status-A', oldEntry);115116const result = runDedicatedEntryUpdate([], existing, createMockAccessor);117118assert.strictEqual(result.size, 0, 'should have no entries');119assert.strictEqual(oldEntry.disposed, true, 'old entry should be disposed');120});121122test('duplicate status IDs - fixed version reuses entry from current update', () => {123// This is the core regression test: when model.dedicated contains124// duplicate IDs (which can happen momentarily when a status is125// re-registered via $setLanguageStatus), the fixed code should126// reuse the entry created for the first occurrence instead of127// creating a second entry that orphans the first.128const existing = new Map<string, MockAccessor>();129const createdEntries: MockAccessor[] = [];130131const result = runDedicatedEntryUpdate(132['status-A', 'status-A'], // duplicate IDs133existing,134(id) => { const e = createMockAccessor(id); createdEntries.push(e); return e; }135);136137// Fixed: only one entry should be created, and it should be updated138// when the duplicate is encountered139assert.strictEqual(createdEntries.length, 1, 'should create only one entry');140assert.strictEqual(result.size, 1, 'result map should have one entry');141assert.strictEqual(createdEntries[0].updateCount, 1, 'entry should be updated once for the duplicate');142assert.strictEqual(createdEntries[0].disposed, false, 'the entry should not be disposed');143});144145test('duplicate status IDs - buggy version leaks entry', () => {146// Demonstrates that the old (buggy) code creates two entries147// for duplicate IDs, orphaning the first one.148const existing = new Map<string, MockAccessor>();149const createdEntries: MockAccessor[] = [];150151const result = runDedicatedEntryUpdateBuggy(152['status-A', 'status-A'], // duplicate IDs153existing,154(id) => { const e = createMockAccessor(id); createdEntries.push(e); return e; }155);156157// Buggy: two entries are created, the first is orphaned (overwritten in map)158assert.strictEqual(createdEntries.length, 2, 'buggy version creates two entries');159assert.strictEqual(result.size, 1, 'result map has one entry (second overwrites first)');160// The first entry is orphaned - it's not in the result map and not disposed161assert.strictEqual(createdEntries[0].disposed, false, 'first entry is NOT disposed (leaked!)');162assert.notStrictEqual(result.get('status-A'), createdEntries[0], 'first entry is not in the result');163assert.strictEqual(result.get('status-A'), createdEntries[1], 'second entry is in the result');164});165166test('duplicate IDs with existing entry - fixed version reuses existing', () => {167// When an existing entry exists and duplicates appear,168// the fixed code should reuse the existing entry for the first169// occurrence and then reuse it again for the duplicate.170const existing = new Map<string, MockAccessor>();171const oldEntry = createMockAccessor('status-A');172existing.set('status-A', oldEntry);173const createdEntries: MockAccessor[] = [];174175const result = runDedicatedEntryUpdate(176['status-A', 'status-A'], // duplicate IDs177existing,178(id) => { const e = createMockAccessor(id); createdEntries.push(e); return e; }179);180181assert.strictEqual(createdEntries.length, 0, 'should not create any new entries');182assert.strictEqual(result.size, 1, 'result map should have one entry');183assert.strictEqual(result.get('status-A'), oldEntry, 'should reuse the existing entry');184assert.strictEqual(oldEntry.updateCount, 2, 'should be updated twice (once per duplicate)');185assert.strictEqual(oldEntry.disposed, false, 'should not be disposed');186});187188test('mixed unique and duplicate IDs', () => {189const existing = new Map<string, MockAccessor>();190const existingB = createMockAccessor('status-B');191existing.set('status-B', existingB);192const createdEntries: MockAccessor[] = [];193194const result = runDedicatedEntryUpdate(195['status-A', 'status-B', 'status-A'], // A appears twice, B once196existing,197(id) => { const e = createMockAccessor(id); createdEntries.push(e); return e; }198);199200assert.strictEqual(createdEntries.length, 1, 'should create one new entry (for first status-A)');201assert.strictEqual(result.size, 2, 'result map should have two entries');202assert.strictEqual(result.get('status-A'), createdEntries[0], 'status-A should use created entry');203assert.strictEqual(createdEntries[0].updateCount, 1, 'status-A entry updated once for duplicate');204assert.strictEqual(result.get('status-B'), existingB, 'status-B should reuse existing entry');205assert.strictEqual(existingB.updateCount, 1, 'status-B entry updated once');206});207});208209210