UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

203 lines • 9.12 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2019 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const assert = require("assert"); const inversify_1 = require("inversify"); const tree_test_container_1 = require("./test/tree-test-container"); const tree_1 = require("./tree"); const tree_model_1 = require("./tree-model"); const tree_label_provider_1 = require("./tree-label-provider"); const mock_tree_model_1 = require("./test/mock-tree-model"); const common_1 = require("../../common"); const promise_util_1 = require("../../common/promise-util"); let ConsistencyTestTree = class ConsistencyTestTree extends tree_1.TreeImpl { constructor() { super(...arguments); this.resolveCounter = 0; } async resolveChildren(parent) { if (parent.id === 'expandable') { const step = async () => { // a predicate to emulate bad timing, i.e. // children of a node gets resolved when a root is changed if (this.root && this.root !== parent.parent) { this.resolveCounter++; return []; } else { await new Promise(resolve => setTimeout(resolve, 10)); return step(); } }; return step(); } return super.resolveChildren(parent); } }; ConsistencyTestTree = tslib_1.__decorate([ (0, inversify_1.injectable)() ], ConsistencyTestTree); /** * Return roots having the same id, but not object identity. */ function createConsistencyTestRoot(rootName) { const children = []; const root = { id: 'root', name: rootName, parent: undefined, children }; const parent = { id: 'expandable', name: 'expandable', parent: root, expanded: true, children: [] }; children.push(parent); return root; } /** * A `TreeImpl` whose `resolveChildren` can be stalled or overridden per node id, to * simulate slow child resolution (e.g. a file system request) in tests. */ let StallingTestTree = class StallingTestTree extends tree_1.TreeImpl { constructor() { super(...arguments); this.stalled = new Map(); this.overrides = new Map(); } /** * Stall the *next* `resolveChildren` for `id` on the returned deferred. Later * resolutions for the same id proceed normally, so the stall cannot leak into * unrelated refreshes (e.g. the cascade of a newly set root). */ stallNextResolve(id) { const gate = new promise_util_1.Deferred(); this.stalled.set(id, gate); return gate; } /** Make the *next* `resolveChildren` for `id` return `children()` instead of the current child nodes. */ overrideNextResolve(id, children) { this.overrides.set(id, children); } resolveChildren(parent) { const gate = this.stalled.get(parent.id); if (gate) { this.stalled.delete(parent.id); return gate.promise; } const override = this.overrides.get(parent.id); if (override) { this.overrides.delete(parent.id); return Promise.resolve(override()); } return super.resolveChildren(parent); } }; StallingTestTree = tslib_1.__decorate([ (0, inversify_1.injectable)() ], StallingTestTree); describe('Tree Consistency', () => { it('setting different tree roots should finish', async () => { const container = (0, tree_test_container_1.createTreeTestContainer)(); container.bind(tree_label_provider_1.TreeLabelProvider).toSelf().inSingletonScope(); const labelProvider = container.get(tree_label_provider_1.TreeLabelProvider); container.bind(ConsistencyTestTree).toSelf(); container.rebind(tree_1.TreeImpl).toService(ConsistencyTestTree); const tree = container.get(ConsistencyTestTree); const model = container.get(tree_model_1.TreeModel); model.root = createConsistencyTestRoot('Foo'); await new Promise(resolve => setTimeout(resolve, 50)); model.root = createConsistencyTestRoot('Bar'); await new Promise(resolve => setTimeout(resolve, 50)); let resolveCounter = tree.resolveCounter; assert.deepStrictEqual(tree.resolveCounter, 1); for (let i = 0; i < 10; i++) { await new Promise(resolve => setTimeout(resolve, 50)); if (resolveCounter === tree.resolveCounter) { assert.deepStrictEqual(tree.resolveCounter, 1); assert.deepStrictEqual(labelProvider.getName(model.root), 'Bar'); return; } resolveCounter = tree.resolveCounter; } assert.ok(false, 'Resolving does not stop, attempts: ' + tree.resolveCounter); }); describe('stale refreshes', () => { let tree; let model; let loggedErrors; beforeEach(async () => { const container = (0, tree_test_container_1.createTreeTestContainer)(); container.bind(StallingTestTree).toSelf(); container.rebind(tree_1.TreeImpl).toService(StallingTestTree); tree = container.get(StallingTestTree); model = container.get(tree_model_1.TreeModel); loggedErrors = []; const logger = container.get(common_1.ILogger); logger.error = async (...args) => { loggedErrors.push(args); }; model.root = mock_tree_model_1.MockTreeModel.HIERARCHICAL_MOCK_ROOT(); // let the refresh cascade triggered by setting the root settle await (0, promise_util_1.timeout)(0); }); it('drops an in-flight refresh when the root is replaced meanwhile', async () => { const stale = model.getNode('1.2'); const gate = tree.stallNextResolve('1.2'); const pendingRefresh = tree.refresh(stale); model.root = mock_tree_model_1.MockTreeModel.HIERARCHICAL_MOCK_ROOT(); gate.resolve(Array.from(stale.children)); assert.strictEqual(await pendingRefresh, undefined); assert.deepStrictEqual(loggedErrors, []); const fresh = model.getNode('1.2'); assert.ok(fresh); assert.notStrictEqual(fresh, stale); }); it('does not resurrect a removed subtree when a stale refresh completes', async () => { const target = model.getNode('1.2'); const gate = tree.stallNextResolve('1.2'); const pendingRefresh = tree.refresh(target); tree_1.CompositeTreeNode.removeChild(target.parent, target, tree); assert.strictEqual(model.getNode('1.2'), undefined); gate.resolve(Array.from(target.children)); assert.strictEqual(await pendingRefresh, undefined); assert.strictEqual(model.getNode('1.2'), undefined); assert.strictEqual(model.getNode('1.2.1'), undefined); }); it('does not re-index a node object that a concurrent ancestor refresh replaced', async () => { const stale = model.getNode('1.2'); const gate = tree.stallNextResolve('1.2'); const pendingRefresh = tree.refresh(stale); // an ancestor refresh yields a *fresh* object for the same id, as trees whose // `resolveChildren` builds new nodes do const parent = model.getNode('1'); const replacement = { id: '1.2', name: '1.2', parent, children: [] }; tree.overrideNextResolve('1', () => parent.children.map(child => child.id === '1.2' ? replacement : child)); await tree.refresh(parent); assert.strictEqual(model.getNode('1.2'), replacement); gate.resolve(Array.from(stale.children)); assert.strictEqual(await pendingRefresh, undefined); assert.deepStrictEqual(loggedErrors, []); // the stale object must not take the replacement's place in the index, nor bring its children back assert.strictEqual(model.getNode('1.2'), replacement); assert.strictEqual(model.getNode('1.2.1'), undefined); }); }); }); //# sourceMappingURL=tree-consistency.spec.js.map