Path: blob/main/src/vs/base/test/common/iterativePaging.test.ts
4778 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 { CancellationToken, CancellationTokenSource } from '../../common/cancellation.js';7import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js';8import { IterativePagedModel, IIterativePager, IIterativePage } from '../../common/paging.js';910function createTestPager(pageSize: number, maxPages: number): IIterativePager<number> {11let currentPage = 0;1213const createPage = (page: number): IIterativePage<number> => {14const start = page * pageSize;15const items: number[] = [];16for (let i = 0; i < pageSize; i++) {17items.push(start + i);18}19const hasMore = page + 1 < maxPages;20return { items, hasMore };21};2223return {24firstPage: createPage(currentPage++),25getNextPage: async (cancellationToken: CancellationToken): Promise<IIterativePage<number>> => {26if (currentPage >= maxPages) {27return { items: [], hasMore: false };28}29return createPage(currentPage++);30}31};32}3334suite('IterativePagedModel', () => {3536const store = ensureNoDisposablesAreLeakedInTestSuite();3738test('initial state', () => {39const pager = createTestPager(10, 3);40const model = store.add(new IterativePagedModel(pager));4142// Initially first page is loaded, so length should be 10 + 1 sentinel43assert.strictEqual(model.length, 11);44assert.strictEqual(model.isResolved(0), true);45assert.strictEqual(model.isResolved(9), true);46assert.strictEqual(model.isResolved(10), false); // sentinel47});4849test('load first page via sentinel access', async () => {50const pager = createTestPager(10, 3);51const model = store.add(new IterativePagedModel(pager));5253// Access an item in the first page (already loaded)54const item = await model.resolve(0, CancellationToken.None);5556assert.strictEqual(item, 0);57assert.strictEqual(model.length, 11); // 10 items + 1 sentinel58assert.strictEqual(model.isResolved(0), true);59assert.strictEqual(model.isResolved(9), true);60assert.strictEqual(model.isResolved(10), false); // sentinel61});6263test('load multiple pages', async () => {64const pager = createTestPager(10, 3);65const model = store.add(new IterativePagedModel(pager));6667// First page already loaded68assert.strictEqual(model.length, 11);6970// Load second page by accessing its sentinel71await model.resolve(10, CancellationToken.None);72assert.strictEqual(model.length, 21); // 20 items + 1 sentinel73assert.strictEqual(model.get(10), 10); // First item of second page7475// Load third (final) page76await model.resolve(20, CancellationToken.None);77assert.strictEqual(model.length, 30); // 30 items, no sentinel (no more pages)78});7980test('onDidIncrementLength event fires correctly', async () => {81const pager = createTestPager(10, 3);82const model = store.add(new IterativePagedModel(pager));83const lengths: number[] = [];8485store.add(model.onDidIncrementLength((length: number) => lengths.push(length)));8687// Load second page88await model.resolve(10, CancellationToken.None);8990assert.strictEqual(lengths.length, 1);91assert.strictEqual(lengths[0], 21); // 20 items + 1 sentinel9293// Load third page94await model.resolve(20, CancellationToken.None);9596assert.strictEqual(lengths.length, 2);97assert.strictEqual(lengths[1], 30); // 30 items, no sentinel98});99100test('accessing regular items does not trigger loading', async () => {101const pager = createTestPager(10, 3);102const model = store.add(new IterativePagedModel(pager));103104const initialLength = model.length;105106// Access items within the loaded range107assert.strictEqual(model.get(5), 5);108assert.strictEqual(model.isResolved(5), true);109110// Length should not change111assert.strictEqual(model.length, initialLength);112});113114test('reaching end of data removes sentinel', async () => {115const pager = createTestPager(10, 3);116const model = store.add(new IterativePagedModel(pager));117118// Load all pages119await model.resolve(10, CancellationToken.None); // Page 2120await model.resolve(20, CancellationToken.None); // Page 3 (final)121122// After loading all data, there should be no more pages123assert.strictEqual(model.length, 30); // Exactly 30 items, no sentinel124125// Accessing resolved items should work126assert.strictEqual(model.isResolved(29), true);127assert.strictEqual(model.isResolved(30), false);128});129130test('concurrent access to sentinel only loads once', async () => {131const pager = createTestPager(10, 3);132const model = store.add(new IterativePagedModel(pager));133134// Access sentinel concurrently135const [item1, item2, item3] = await Promise.all([136model.resolve(10, CancellationToken.None),137model.resolve(10, CancellationToken.None),138model.resolve(10, CancellationToken.None)139]);140141// All should get the same item142assert.strictEqual(item1, 10);143assert.strictEqual(item2, 10);144assert.strictEqual(item3, 10);145assert.strictEqual(model.length, 21); // 20 items + 1 sentinel146});147148test('empty pager with no items', () => {149const emptyPager: IIterativePager<number> = {150firstPage: { items: [], hasMore: false },151getNextPage: async () => ({ items: [], hasMore: false })152};153const model = store.add(new IterativePagedModel(emptyPager));154155assert.strictEqual(model.length, 0);156assert.strictEqual(model.isResolved(0), false);157});158159test('single page pager with no more pages', () => {160const singlePagePager: IIterativePager<number> = {161firstPage: { items: [1, 2, 3], hasMore: false },162getNextPage: async () => ({ items: [], hasMore: false })163};164const model = store.add(new IterativePagedModel(singlePagePager));165166assert.strictEqual(model.length, 3); // No sentinel167assert.strictEqual(model.isResolved(0), true);168assert.strictEqual(model.isResolved(2), true);169assert.strictEqual(model.isResolved(3), false);170assert.strictEqual(model.get(0), 1);171assert.strictEqual(model.get(2), 3);172});173174test('accessing item beyond loaded range throws', () => {175const pager = createTestPager(10, 3);176const model = store.add(new IterativePagedModel(pager));177178// Try to access item beyond current length179assert.throws(() => model.get(15), /Item not resolved yet/);180});181182test('resolving item beyond all pages throws', async () => {183const pager = createTestPager(10, 3);184const model = store.add(new IterativePagedModel(pager));185186// Load all pages187await model.resolve(10, CancellationToken.None);188await model.resolve(20, CancellationToken.None);189190// Try to resolve beyond the last item191await assert.rejects(192async () => model.resolve(30, CancellationToken.None),193/Index out of bounds/194);195});196197test('cancelled token during initial resolve', async () => {198const pager = createTestPager(10, 3);199const model = store.add(new IterativePagedModel(pager));200201const cts = new CancellationTokenSource();202cts.cancel();203204await assert.rejects(205async () => model.resolve(0, cts.token),206/Canceled/207);208});209210test('event fires for each page load', async () => {211const pager = createTestPager(5, 4);212const model = store.add(new IterativePagedModel(pager));213const lengths: number[] = [];214215store.add(model.onDidIncrementLength((length: number) => lengths.push(length)));216217// Initially has first page (5 items + 1 sentinel = 6)218assert.strictEqual(model.length, 6);219220// Load page 2221await model.resolve(5, CancellationToken.None);222assert.deepStrictEqual(lengths, [11]); // 10 items + 1 sentinel223224// Load page 3225await model.resolve(10, CancellationToken.None);226assert.deepStrictEqual(lengths, [11, 16]); // 15 items + 1 sentinel227228// Load page 4 (final)229await model.resolve(15, CancellationToken.None);230assert.deepStrictEqual(lengths, [11, 16, 20]); // 20 items, no sentinel231});232233test('sequential page loads work correctly', async () => {234const pager = createTestPager(5, 3);235const model = store.add(new IterativePagedModel(pager));236237// Load pages sequentially238for (let page = 1; page < 3; page++) {239const sentinelIndex = page * 5;240await model.resolve(sentinelIndex, CancellationToken.None);241}242243// Verify all items are accessible244assert.strictEqual(model.length, 15); // 3 pages * 5 items, no sentinel245for (let i = 0; i < 15; i++) {246assert.strictEqual(model.get(i), i);247assert.strictEqual(model.isResolved(i), true);248}249});250251test('accessing items after loading all pages', async () => {252const pager = createTestPager(10, 2);253const model = store.add(new IterativePagedModel(pager));254255// Load second page256await model.resolve(10, CancellationToken.None);257258// No sentinel after loading all pages259assert.strictEqual(model.length, 20);260assert.strictEqual(model.isResolved(19), true);261assert.strictEqual(model.isResolved(20), false);262263// All items should be accessible264for (let i = 0; i < 20; i++) {265assert.strictEqual(model.get(i), i);266}267});268269test('pager with varying page sizes', async () => {270let pageNum = 0;271const varyingPager: IIterativePager<string> = {272firstPage: { items: ['a', 'b', 'c'], hasMore: true },273getNextPage: async (): Promise<IIterativePage<string>> => {274pageNum++;275if (pageNum === 1) {276return { items: ['d', 'e'], hasMore: true };277} else if (pageNum === 2) {278return { items: ['f', 'g', 'h', 'i'], hasMore: false };279}280return { items: [], hasMore: false };281}282};283284const model = store.add(new IterativePagedModel(varyingPager));285286assert.strictEqual(model.length, 4); // 3 items + 1 sentinel287288// Load second page (2 items)289await model.resolve(3, CancellationToken.None);290assert.strictEqual(model.length, 6); // 5 items + 1 sentinel291assert.strictEqual(model.get(3), 'd');292293// Load third page (4 items)294await model.resolve(5, CancellationToken.None);295assert.strictEqual(model.length, 9); // 9 items, no sentinel296assert.strictEqual(model.get(5), 'f');297assert.strictEqual(model.get(8), 'i');298});299});300301302