Path: blob/main/components/gitpod-protocol/src/generate-async-generator.spec.ts
2498 views
/**1* Copyright (c) 2023 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { suite, test } from "@testdeck/mocha";7import * as chai from "chai";89import { generateAsyncGenerator } from "./generate-async-generator";10import { Disposable } from "./util/disposable";1112const expect = chai.expect;1314function watchWith(times: number, listener: (value: number) => void): Disposable {15let i = 0;16const cancel = setInterval(() => {17if (i < times) {18listener(i++);19}20}, 100);21return {22dispose: () => {23clearInterval(cancel);24},25};26}2728const error = new Error("Test error");29interface Ref {30isDisposed: boolean;31result: number[];32watchStarted: boolean;33}3435interface Option {36errorAfter?: number;37times: number;38abortAfterMs?: number;39setupError?: boolean;40}4142function watchIterator(ref: Ref, opts: Option) {43const abortController = new AbortController();44setTimeout(() => {45abortController.abort();46}, opts.abortAfterMs ?? 600);47return generateAsyncGenerator<number>(48(sink) => {49try {50if (opts.setupError) {51throw error;52}53ref.watchStarted = true;54const dispose = watchWith(opts.times, (v) => {55if (opts.errorAfter && opts.errorAfter === v) {56sink.fail(error);57return;58}59sink.push(v);60});61return () => {62ref.isDisposed = true;63dispose.dispose();64};65} catch (e) {66sink.fail(e as any as Error);67}68},69{ signal: abortController.signal },70);71}7273@suite74class TestGenerateAsyncGenerator {75@test public async "happy path"() {76const ref: Ref = { isDisposed: false, result: [], watchStarted: false };77const it = watchIterator(ref, { times: 5 });78try {79for await (const v of it) {80ref.result.push(v);81}82expect.fail("should throw error");83} catch (e) {84if (ref.watchStarted) {85expect(ref.isDisposed).to.be.equal(true);86}87expect(e.message).to.be.equal("cancelled");88expect(ref.result.length).to.be.equal(5);89ref.result.forEach((v, i) => expect(v).to.be.equal(i));90expect(ref.isDisposed).to.be.equal(true);91}92}9394@test public async "should be stopped after abort signal is triggered"() {95const ref: Ref = { isDisposed: false, result: [], watchStarted: false };96const it = watchIterator(ref, { times: 5, abortAfterMs: 120 });97try {98for await (const v of it) {99ref.result.push(v);100}101expect.fail("should throw error");102} catch (e) {103if (ref.watchStarted) {104expect(ref.isDisposed).to.be.equal(true);105}106expect(e.message).to.be.equal("cancelled");107expect(ref.result[0]).to.be.equal(0);108expect(ref.result.length).to.be.equal(1);109ref.result.forEach((v, i) => expect(v).to.be.equal(i));110expect(ref.isDisposed).to.be.equal(true);111}112}113114@test public async "should throw error if setup throws"() {115const ref: Ref = { isDisposed: false, result: [], watchStarted: false };116const it = watchIterator(ref, { times: 5, setupError: true });117try {118for await (const v of it) {119ref.result.push(v);120}121expect.fail("should throw error");122} catch (e) {123if (ref.watchStarted) {124expect(ref.isDisposed).to.be.equal(true);125}126expect(e).to.be.equal(error);127expect(ref.result.length).to.be.equal(0);128ref.result.forEach((v, i) => expect(v).to.be.equal(i));129expect(ref.isDisposed).to.be.equal(false);130}131}132133@test public async "should propagate errors from sink.next"() {134const ref: Ref = { isDisposed: false, result: [], watchStarted: false };135const it = watchIterator(ref, { times: 5, errorAfter: 2 });136try {137for await (const v of it) {138ref.result.push(v);139}140expect.fail("should throw error");141} catch (e) {142if (ref.watchStarted) {143expect(ref.isDisposed).to.be.equal(true);144}145expect(e).to.be.equal(error);146expect(ref.result.length).to.be.equal(2);147ref.result.forEach((v, i) => expect(v).to.be.equal(i));148expect(ref.isDisposed).to.be.equal(true);149}150}151152@test public async "should not start iterator if pre throw error in an iterator"() {153const ref: Ref = { isDisposed: false, result: [], watchStarted: false };154const it = this.mockWatchWorkspaceStatus(ref, { times: 5, errorAfter: 2 });155try {156for await (const v of it) {157ref.result.push(v);158}159expect.fail("should throw error");160} catch (e) {161expect(ref.watchStarted).to.be.equal(false);162if (ref.watchStarted) {163expect(ref.isDisposed).to.be.equal(true);164}165expect(e.message).to.be.equal("Should throw error");166expect(ref.result.length).to.be.equal(0);167ref.result.forEach((v, i) => expect(v).to.be.equal(i));168expect(ref.isDisposed).to.be.equal(false);169}170}171172async *mockWatchWorkspaceStatus(ref: Ref, option: Option): AsyncIterable<number> {173const shouldThrow = true;174if (shouldThrow) {175throw new Error("Should throw error");176}177const it = watchIterator(ref, option);178for await (const item of it) {179yield item;180}181}182}183184module.exports = new TestGenerateAsyncGenerator(); // Only to circumvent no usage warning :-/185186187