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