summaryrefslogtreecommitdiff
path: root/test/parallel/test-wasm-web-api.js
blob: d4a81794f80eb31207196e4882171ad959df8457 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');

const assert = require('assert');
const events = require('events');
const fs = require('fs/promises');
const { createServer } = require('http');

assert.strictEqual(typeof WebAssembly.compileStreaming, 'function');
assert.strictEqual(typeof WebAssembly.instantiateStreaming, 'function');

const simpleWasmBytes = fixtures.readSync('simple.wasm');

// Sets up an HTTP server with the given response handler and calls fetch() to
// obtain a Response from the newly created server.
async function testRequest(handler) {
  const server = createServer((_, res) => handler(res)).unref().listen(0);
  await events.once(server, 'listening');
  const { port } = server.address();
  return fetch(`http://127.0.0.1:${port}/foo.wasm`);
}

// Runs the given function both with the promise itself and as a continuation
// of the promise. We use this to test that the API accepts not just a Response
// but also a Promise that resolves to a Response.
function withPromiseAndResolved(makePromise, consume) {
  return Promise.all([
    consume(makePromise()),
    makePromise().then(consume),
  ]);
}

// The makeResponsePromise function must return a Promise that resolves to a
// Response. The checkResult function receives the Promise returned by
// WebAssembly.compileStreaming and must return a Promise itself.
function testCompileStreaming(makeResponsePromise, checkResult) {
  return withPromiseAndResolved(
    common.mustCall(makeResponsePromise, 2),
    common.mustCall((response) => {
      return checkResult(WebAssembly.compileStreaming(response));
    }, 2)
  );
}

function testCompileStreamingSuccess(makeResponsePromise) {
  return testCompileStreaming(makeResponsePromise, async (modPromise) => {
    const mod = await modPromise;
    assert.strictEqual(mod.constructor, WebAssembly.Module);
  });
}

function testCompileStreamingRejection(makeResponsePromise, rejection) {
  return testCompileStreaming(makeResponsePromise, (modPromise) => {
    assert.strictEqual(modPromise.constructor, Promise);
    return assert.rejects(modPromise, rejection);
  });
}

function testCompileStreamingSuccessUsingFetch(responseCallback) {
  return testCompileStreamingSuccess(() => testRequest(responseCallback));
}

function testCompileStreamingRejectionUsingFetch(responseCallback, rejection) {
  return testCompileStreamingRejection(() => testRequest(responseCallback),
                                       rejection);
}

(async () => {
  // A non-Response should cause a TypeError.
  for (const invalid of [undefined, null, 0, true, 'foo', {}, [], Symbol()]) {
    await withPromiseAndResolved(() => Promise.resolve(invalid), (arg) => {
      return assert.rejects(() => WebAssembly.compileStreaming(arg), {
        name: 'TypeError',
        code: 'ERR_INVALID_ARG_TYPE',
        message: /^The "source" argument .*$/
      });
    });
  }

  // When given a Promise, any rejection should be propagated as-is.
  {
    const err = new RangeError('foo');
    await assert.rejects(() => {
      return WebAssembly.compileStreaming(Promise.reject(err));
    }, (actualError) => actualError === err);
  }

  // A valid WebAssembly file with the correct MIME type.
  await testCompileStreamingSuccessUsingFetch((res) => {
    res.setHeader('Content-Type', 'application/wasm');
    res.end(simpleWasmBytes);
  });

  // The same valid WebAssembly file with the same MIME type, but using a
  // Response whose body is a Buffer instead of calling fetch().
  await testCompileStreamingSuccess(() => {
    return Promise.resolve(new Response(simpleWasmBytes, {
      status: 200,
      headers: { 'Content-Type': 'application/wasm' }
    }));
  });

  // The same valid WebAssembly file with the same MIME type, but using a
  // Response whose body is a ReadableStream instead of calling fetch().
  await testCompileStreamingSuccess(async () => {
    const handle = await fs.open(fixtures.path('simple.wasm'));
    const stream = handle.readableWebStream();
    return Promise.resolve(new Response(stream, {
      status: 200,
      headers: { 'Content-Type': 'application/wasm' }
    }));
  });

  // A larger valid WebAssembly file with the correct MIME type that causes the
  // client to pass it to the compiler in many separate chunks. For this, we use
  // the same WebAssembly file as in the previous test but insert useless custom
  // sections into the WebAssembly module to increase the file size without
  // changing the relevant contents.
  await testCompileStreamingSuccessUsingFetch((res) => {
    res.setHeader('Content-Type', 'application/wasm');

    // Send the WebAssembly magic and version first.
    res.write(simpleWasmBytes.slice(0, 8), common.mustCall());

    // Construct a 4KiB custom section.
    const customSection = Buffer.concat([
      Buffer.from([
        0,        // Custom section.
        134, 32,  // (134 & 0x7f) + 0x80 * 32 = 6 + 4096 bytes in this section.
        5,        // The length of the following section name.
      ]),
      Buffer.from('?'.repeat(5)),      // The section name
      Buffer.from('\0'.repeat(4096)),  // The actual section data
    ]);

    // Now repeatedly send useless custom sections. These have no use for the
    // WebAssembly compiler but they are syntactically valid. The client has to
    // keep reading the stream until the very end to obtain the relevant
    // sections within the module. This adds up to a few hundred kibibytes.
    (function next(i) {
      if (i < 100) {
        while (res.write(customSection));
        res.once('drain', () => next(i + 1));
      } else {
        // End the response body with the actual module contents.
        res.end(simpleWasmBytes.slice(8));
      }
    })(0);
  });

  // A valid WebAssembly file with an empty parameter in the (otherwise valid)
  // MIME type.
  await testCompileStreamingRejectionUsingFetch((res) => {
    res.setHeader('Content-Type', 'application/wasm;');
    res.end(simpleWasmBytes);
  }, {
    name: 'TypeError',
    code: 'ERR_WEBASSEMBLY_RESPONSE',
    message: 'WebAssembly response has unsupported MIME type ' +
             "'application/wasm;'"
  });

  // A valid WebAssembly file with an invalid MIME type.
  await testCompileStreamingRejectionUsingFetch((res) => {
    res.setHeader('Content-Type', 'application/octet-stream');
    res.end(simpleWasmBytes);
  }, {
    name: 'TypeError',
    code: 'ERR_WEBASSEMBLY_RESPONSE',
    message: 'WebAssembly response has unsupported MIME type ' +
             "'application/octet-stream'"
  });

  // HTTP status code indiciating an error.
  await testCompileStreamingRejectionUsingFetch((res) => {
    res.statusCode = 418;
    res.setHeader('Content-Type', 'application/wasm');
    res.end(simpleWasmBytes);
  }, {
    name: 'TypeError',
    code: 'ERR_WEBASSEMBLY_RESPONSE',
    message: /^WebAssembly response has status code 418$/
  });

  // HTTP status code indiciating an error, but using a Response whose body is
  // a Buffer instead of calling fetch().
  await testCompileStreamingSuccess(() => {
    return Promise.resolve(new Response(simpleWasmBytes, {
      status: 200,
      headers: { 'Content-Type': 'application/wasm' }
    }));
  });

  // Extra bytes after the WebAssembly file.
  await testCompileStreamingRejectionUsingFetch((res) => {
    res.setHeader('Content-Type', 'application/wasm');
    res.end(Buffer.concat([simpleWasmBytes, Buffer.from('foo')]));
  }, {
    name: 'CompileError',
    message: /^WebAssembly\.compileStreaming\(\): .*$/
  });

  // Missing bytes at the end of the WebAssembly file.
  await testCompileStreamingRejectionUsingFetch((res) => {
    res.setHeader('Content-Type', 'application/wasm');
    res.end(simpleWasmBytes.subarray(0, simpleWasmBytes.length - 3));
  }, {
    name: 'CompileError',
    message: /^WebAssembly\.compileStreaming\(\): .*$/
  });

  // Incomplete HTTP response body. The TypeError might come as a surprise, but
  // it originates from within fetch().
  await testCompileStreamingRejectionUsingFetch((res) => {
    res.setHeader('Content-Length', simpleWasmBytes.length);
    res.setHeader('Content-Type', 'application/wasm');
    res.write(simpleWasmBytes.slice(0, 5), common.mustSucceed(() => {
      res.destroy();
    }));
  }, {
    name: 'TypeError',
    message: /terminated/
  });

  // Test "Developer-Facing Display Conventions" described in the WebAssembly
  // Web API specification.
  await testCompileStreaming(() => testRequest((res) => {
    // Respond with a WebAssembly module that only exports a single function,
    // which only contains an 'unreachable' instruction.
    res.setHeader('Content-Type', 'application/wasm');
    res.end(fixtures.readSync('crash.wasm'));
  }), async (modPromise) => {
    // Call the WebAssembly function and check that the error stack contains the
    // correct "WebAssembly location" as per the specification.
    const mod = await modPromise;
    const instance = new WebAssembly.Instance(mod);
    assert.throws(() => instance.exports.crash(), (err) => {
      const stack = err.stack.split(/\n/g);
      assert.strictEqual(stack[0], 'RuntimeError: unreachable');
      assert.match(stack[1],
                   /^\s*at http:\/\/127\.0\.0\.1:\d+\/foo\.wasm:wasm-function\[0\]:0x22$/);
      return true;
    });
  });
})().then(common.mustCall());