File
Blob: src/workerd/jsg/iterator-test.c++
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #include "jsg-test.h" |
| 6 | |
| 7 | namespace workerd::jsg::test { |
| 8 | namespace { |
| 9 | |
| 10 | V8System v8System; |
| 11 | |
| 12 | struct GeneratorContext: public Object, public ContextGlobal { |
| 13 | |
| 14 | kj::Array<kj::String> generatorTest(Lock& js, Generator<kj::String> generator) { |
| 15 | kj::Vector<kj::String> items; |
| 16 | while (true) { |
| 17 | KJ_IF_SOME(item, generator.next(js)) { |
| 18 | items.add(kj::mv(item)); |
| 19 | } else { |
| 20 | break; |
| 21 | } |
| 22 | } |
| 23 | return items.releaseAsArray(); |
| 24 | } |
| 25 | |
| 26 | uint generatorErrorTest(Lock& js, Generator<kj::String> generator) { |
| 27 | uint count = 0; |
| 28 | |
| 29 | // First call to next() should succeed and return "a" |
| 30 | KJ_IF_SOME(val, generator.next(js)) { |
| 31 | KJ_ASSERT(val == "a"); |
| 32 | ++count; |
| 33 | } |
| 34 | |
| 35 | // Second call - we'll throw an error, which should trigger the generator's |
| 36 | // throw handler (the catch block), which yields "c" |
| 37 | KJ_IF_SOME(val, generator.throw_(js, js.v8Ref<v8::Value>(js.str("boom"_kj)))) { |
| 38 | KJ_ASSERT(val == "c"); |
| 39 | ++count; |
| 40 | } |
| 41 | |
| 42 | return count; |
| 43 | } |
| 44 | |
| 45 | uint sequenceOfSequenceTest(Lock& js, Sequence<Sequence<kj::String>> sequence) { |
| 46 | uint count = 0; |
| 47 | for (auto& mem: sequence) { |
| 48 | count += mem.size(); |
| 49 | } |
| 50 | return count; |
| 51 | } |
| 52 | |
| 53 | uint asyncGeneratorTest(Lock& js, AsyncGenerator<kj::String> generator) { |
| 54 | uint count = 0; |
| 55 | bool finished = false; |
| 56 | |
| 57 | // Get first item |
| 58 | generator.next(js) |
| 59 | .then(js, [&count, &generator](auto& js, auto value) { |
| 60 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "a"); |
| 61 | ++count; |
| 62 | |
| 63 | // After getting first item, call return_() to terminate early |
| 64 | return generator.return_(js, kj::str("foo")).then(js, [&count](auto& js, auto value) { |
| 65 | // return_() should give us back "foo" and mark as done |
| 66 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "foo"); |
| 67 | ++count; |
| 68 | return js.resolvedPromise(); |
| 69 | }); |
| 70 | }).then(js, [&finished](auto& js) { |
| 71 | finished = true; |
| 72 | return js.resolvedPromise(); |
| 73 | }); |
| 74 | |
| 75 | js.runMicrotasks(); |
| 76 | |
| 77 | KJ_ASSERT(finished); |
| 78 | KJ_ASSERT(count == 2); |
| 79 | |
| 80 | return count; |
| 81 | } |
| 82 | |
| 83 | uint asyncGeneratorErrorTest(Lock& js, AsyncGenerator<kj::String> generator) { |
| 84 | uint count = 0; |
| 85 | bool finished = false; |
| 86 | |
| 87 | // First call to next() should succeed and return "a" |
| 88 | generator.next(js) |
| 89 | .then(js, [&count, &generator](auto& js, auto value) { |
| 90 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "a"); |
| 91 | ++count; |
| 92 | |
| 93 | // Second call - throw an error, which should trigger the generator's |
| 94 | // throw handler (the catch block), which yields "c" |
| 95 | return generator.throw_(js, js.template v8Ref<v8::Value>(js.str("boom"_kj))) |
| 96 | .then(js, [&count](auto& js, auto value) { |
| 97 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "c"); |
| 98 | ++count; |
| 99 | return js.resolvedPromise(); |
| 100 | }); |
| 101 | }).then(js, [&finished](auto& js) { |
| 102 | finished = true; |
| 103 | return js.resolvedPromise(); |
| 104 | }); |
| 105 | |
| 106 | js.runMicrotasks(); |
| 107 | |
| 108 | KJ_ASSERT(finished); |
| 109 | KJ_ASSERT(count == 2); |
| 110 | |
| 111 | return count; |
| 112 | } |
| 113 | |
| 114 | void manualAsyncGeneratorTest(Lock& js, AsyncGenerator<kj::String> generator) { |
| 115 | uint calls = 0; |
| 116 | generator.next(js).then(js, [&calls](auto& js, auto value) { |
| 117 | calls++; |
| 118 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "a"); |
| 119 | return js.resolvedPromise(); |
| 120 | }); |
| 121 | |
| 122 | generator.next(js).then(js, [&calls](auto& js, auto value) { |
| 123 | calls++; |
| 124 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "b"); |
| 125 | return js.resolvedPromise(); |
| 126 | }); |
| 127 | |
| 128 | generator.next(js).then(js, [&calls](auto& js, auto value) { |
| 129 | calls++; |
| 130 | KJ_ASSERT(value == kj::none); |
| 131 | }); |
| 132 | |
| 133 | js.runMicrotasks(); |
| 134 | KJ_ASSERT(calls == 3); |
| 135 | } |
| 136 | |
| 137 | void manualAsyncGeneratorTestEarlyReturn(Lock& js, AsyncGenerator<kj::String> generator) { |
| 138 | uint calls = 0; |
| 139 | generator.next(js).then(js, [&calls](auto& js, auto value) { |
| 140 | calls++; |
| 141 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "a"); |
| 142 | return js.resolvedPromise(); |
| 143 | }); |
| 144 | |
| 145 | generator.return_(js, kj::str("foo")).then(js, [&calls](auto& js, auto value) { |
| 146 | calls++; |
| 147 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "foo"); |
| 148 | return js.resolvedPromise(); |
| 149 | }); |
| 150 | |
| 151 | generator.next(js).then(js, [&calls](auto& js, auto value) { |
| 152 | calls++; |
| 153 | KJ_ASSERT(value == kj::none); |
| 154 | }); |
| 155 | |
| 156 | js.runMicrotasks(); |
| 157 | KJ_ASSERT(calls == 3); |
| 158 | } |
| 159 | |
| 160 | void manualAsyncGeneratorTestThrow(Lock& js, AsyncGenerator<kj::String> generator) { |
| 161 | uint calls = 0; |
| 162 | generator.next(js).then(js, [&calls](auto& js, auto value) { |
| 163 | calls++; |
| 164 | KJ_ASSERT(KJ_ASSERT_NONNULL(value) == "a"); |
| 165 | return js.resolvedPromise(); |
| 166 | }); |
| 167 | |
| 168 | // The default implementation of throw on the Async generator will result in a |
| 169 | // rejected promise being returned by generator.throw_(...) |
| 170 | generator.throw_(js, js.v8Ref<v8::Value>(js.str("boom"_kj))) |
| 171 | .catch_(js, [&calls](jsg::Lock& js, jsg::Value exception) { |
| 172 | calls++; |
| 173 | return kj::Maybe<kj::String>(kj::none); |
| 174 | }); |
| 175 | |
| 176 | generator.next(js).then(js, [&calls](auto& js, auto value) { |
| 177 | calls++; |
| 178 | KJ_ASSERT(value == kj::none); |
| 179 | }); |
| 180 | |
| 181 | js.runMicrotasks(); |
| 182 | KJ_ASSERT(calls == 3); |
| 183 | } |
| 184 | |
| 185 | struct Test { |
| 186 | int foo; |
| 187 | JSG_STRUCT(foo); |
| 188 | }; |
| 189 | |
| 190 | void generatorWrongType(Lock& js, Generator<Test> generator) { |
| 191 | // This should throw a type error when trying to unwrap the value |
| 192 | generator.next(js); |
| 193 | } |
| 194 | |
| 195 | void generatorReturnNotCallable(Lock& js, Generator<kj::String> generator) { |
| 196 | // Per the GetMethod spec, if the 'return' property exists on the iterator |
| 197 | // but is not callable, calling return_() should throw a TypeError. |
| 198 | generator.return_(js); |
| 199 | } |
| 200 | |
| 201 | void asyncGeneratorReturnNotCallable(Lock& js, AsyncGenerator<kj::String> generator) { |
| 202 | // Per the GetMethod spec, if the 'return' property exists on the async iterator |
| 203 | // but is not callable, calling return_() should produce a rejected promise with |
| 204 | // a TypeError. |
| 205 | bool gotRejection = false; |
| 206 | generator.return_(js).catch_(js, [&gotRejection](jsg::Lock& js, jsg::Value exception) { |
| 207 | gotRejection = true; |
| 208 | return kj::Maybe<kj::String>(kj::none); |
| 209 | }); |
| 210 | js.runMicrotasks(); |
| 211 | KJ_ASSERT(gotRejection); |
| 212 | } |
| 213 | |
| 214 | JSG_RESOURCE_TYPE(GeneratorContext) { |
| 215 | JSG_METHOD(generatorTest); |
| 216 | JSG_METHOD(generatorErrorTest); |
| 217 | JSG_METHOD(sequenceOfSequenceTest); |
| 218 | JSG_METHOD(generatorWrongType); |
| 219 | JSG_METHOD(generatorReturnNotCallable); |
| 220 | JSG_METHOD(asyncGeneratorTest); |
| 221 | JSG_METHOD(asyncGeneratorErrorTest); |
| 222 | JSG_METHOD(manualAsyncGeneratorTest); |
| 223 | JSG_METHOD(manualAsyncGeneratorTestEarlyReturn); |
| 224 | JSG_METHOD(manualAsyncGeneratorTestThrow); |
| 225 | JSG_METHOD(asyncGeneratorReturnNotCallable); |
| 226 | } |
| 227 | }; |
| 228 | JSG_DECLARE_ISOLATE_TYPE(GeneratorIsolate, GeneratorContext, GeneratorContext::Test); |
| 229 | |
| 230 | KJ_TEST("Generator works") { |
| 231 | Evaluator<GeneratorContext, GeneratorIsolate> e(v8System); |
| 232 | |
| 233 | e.expectEval("generatorTest([undefined,2,3])", "object", "undefined,2,3"); |
| 234 | |
| 235 | e.expectEval( |
| 236 | "function* gen() { try { yield 'a'; yield 'b'; yield 'c'; } finally { yield 'd'; } };" |
| 237 | "generatorTest(gen())", |
| 238 | "object", "a,b,c,d"); |
| 239 | |
| 240 | e.expectEval("function* gen() { try { yield 'a'; yield 'b'; } catch { yield 'c' } }; " |
| 241 | "generatorErrorTest(gen())", |
| 242 | "number", "2"); |
| 243 | |
| 244 | e.expectEval("sequenceOfSequenceTest([['a','b'],['c', undefined]])", "number", "4"); |
| 245 | |
| 246 | e.expectEval("generatorWrongType(['a'])", "throws", |
| 247 | "TypeError: Incorrect type: the provided value is not of type 'Test'."); |
| 248 | |
| 249 | // Per the GetMethod spec, if the 'return' property exists but is not callable, |
| 250 | // calling return_() should throw a TypeError. |
| 251 | e.expectEval("var iter = { [Symbol.iterator]() { return this; }, " |
| 252 | "next() { return { value: 'a', done: false }; }, " |
| 253 | "return: 42 }; " |
| 254 | "generatorReturnNotCallable(iter)", |
| 255 | "throws", "TypeError: Property 'return' is not a function"); |
| 256 | } |
| 257 | |
| 258 | KJ_TEST("AsyncGenerator works") { |
| 259 | Evaluator<GeneratorContext, GeneratorIsolate> e(v8System); |
| 260 | |
| 261 | e.expectEval( |
| 262 | "async function* foo() { yield 'a'; yield 'b'; }; asyncGeneratorTest(foo());", "number", "2"); |
| 263 | |
| 264 | e.expectEval("async function* gen() { try { yield 'a'; yield 'b'; } catch { yield 'c' } }; " |
| 265 | "asyncGeneratorErrorTest(gen())", |
| 266 | "number", "2"); |
| 267 | |
| 268 | e.expectEval("manualAsyncGeneratorTest(async function* foo() { yield 'a'; yield 'b'; }())", |
| 269 | "undefined", "undefined"); |
| 270 | |
| 271 | e.expectEval("manualAsyncGeneratorTestEarlyReturn(async function* foo() " |
| 272 | "{ yield 'a'; yield 'b'; }())", |
| 273 | "undefined", "undefined"); |
| 274 | |
| 275 | // e.expectEval("manualAsyncGeneratorTestThrow(async function* foo() { yield 'a'; yield 'b'; }())", |
| 276 | // "undefined", "undefined"); |
| 277 | |
| 278 | // Per the GetMethod spec, if the 'return' property exists but is not callable, |
| 279 | // calling return_() should produce a rejected promise with a TypeError. |
| 280 | e.expectEval("var iter = { [Symbol.asyncIterator]() { return this; }, " |
| 281 | "next() { return Promise.resolve({ value: 'a', done: false }); }, " |
| 282 | "return: 42 }; " |
| 283 | "asyncGeneratorReturnNotCallable(iter)", |
| 284 | "undefined", "undefined"); |
| 285 | } |
| 286 | |
| 287 | } // namespace |
| 288 | } // namespace workerd::jsg::test |