Fixes default log output to console for macOS
[sqlcipher.git] / ext / wasm / demo-worker1.js
blob60f5e8dec01de73579d2af0ddd688b184299cf54
1 /*
2   2022-05-22
4   The author disclaims copyright to this source code.  In place of a
5   legal notice, here is a blessing:
7   *   May you do good and not evil.
8   *   May you find forgiveness for yourself and forgive others.
9   *   May you share freely, never taking more than you give.
11   ***********************************************************************
13   A basic test script for sqlite3-worker1.js.
15   Note that the wrapper interface demonstrated in
16   demo-worker1-promiser.js is much easier to use from client code, as it
17   lacks the message-passing acrobatics demonstrated in this file.
19 'use strict';
20 (function(){
21   const T = self.SqliteTestUtil;
22   const SW = new Worker("jswasm/sqlite3-worker1.js");
23   const DbState = {
24     id: undefined
25   };
26   const eOutput = document.querySelector('#test-output');
27   const log = console.log.bind(console);
28   const logHtml = function(cssClass,...args){
29     log.apply(this, args);
30     const ln = document.createElement('div');
31     if(cssClass) ln.classList.add(cssClass);
32     ln.append(document.createTextNode(args.join(' ')));
33     eOutput.append(ln);
34   };
35   const warn = console.warn.bind(console);
36   const error = console.error.bind(console);
37   const toss = (...args)=>{throw new Error(args.join(' '))};
39   SW.onerror = function(event){
40     error("onerror",event);
41   };
43   let startTime;
45   /**
46      A queue for callbacks which are to be run in response to async
47      DB commands. See the notes in runTests() for why we need
48      this. The event-handling plumbing of this file requires that
49      any DB command which includes a `messageId` property also have
50      a queued callback entry, as the existence of that property in
51      response payloads is how it knows whether or not to shift an
52      entry off of the queue.
53   */
54   const MsgHandlerQueue = {
55     queue: [],
56     id: 0,
57     push: function(type,callback){
58       this.queue.push(callback);
59       return type + '-' + (++this.id);
60     },
61     shift: function(){
62       return this.queue.shift();
63     }
64   };
66   const testCount = ()=>{
67     logHtml("","Total test count:",T.counter+". Total time =",(performance.now() - startTime),"ms");
68   };
70   const logEventResult = function(ev){
71     const evd = ev.result;
72     logHtml(evd.errorClass ? 'error' : '',
73             "runOneTest",ev.messageId,"Worker time =",
74             (ev.workerRespondTime - ev.workerReceivedTime),"ms.",
75             "Round-trip event time =",
76             (performance.now() - ev.departureTime),"ms.",
77             (evd.errorClass ? evd.message : "")//, JSON.stringify(evd)
78            );
79   };
81   const runOneTest = function(eventType, eventArgs, callback){
82     T.assert(eventArgs && 'object'===typeof eventArgs);
83     /* ^^^ that is for the testing and messageId-related code, not
84        a hard requirement of all of the Worker-exposed APIs. */
85     const messageId = MsgHandlerQueue.push(eventType,function(ev){
86       logEventResult(ev);
87       if(callback instanceof Function){
88         callback(ev);
89         testCount();
90       }
91     });
92     const msg = {
93       type: eventType,
94       args: eventArgs,
95       dbId: DbState.id,
96       messageId: messageId,
97       departureTime: performance.now()
98     };
99     log("Posting",eventType,"message to worker dbId="+(DbState.id||'default')+':',msg);
100     SW.postMessage(msg);
101   };
103   /** Methods which map directly to onmessage() event.type keys.
104       They get passed the inbound event.data. */
105   const dbMsgHandler = {
106     open: function(ev){
107       DbState.id = ev.dbId;
108       log("open result",ev);
109     },
110     exec: function(ev){
111       log("exec result",ev);
112     },
113     export: function(ev){
114       log("export result",ev);
115     },
116     error: function(ev){
117       error("ERROR from the worker:",ev);
118       logEventResult(ev);
119     },
120     resultRowTest1: function f(ev){
121       if(undefined === f.counter) f.counter = 0;
122       if(null === ev.rowNumber){
123         /* End of result set. */
124         T.assert(undefined === ev.row)
125           .assert(Array.isArray(ev.columnNames))
126           .assert(ev.columnNames.length);
127       }else{
128         T.assert(ev.rowNumber > 0);
129         ++f.counter;
130       }
131       //log("exec() result row:",ev);
132       T.assert(null === ev.rowNumber || 'number' === typeof ev.row.b);
133     }
134   };
136   /**
137      "The problem" now is that the test results are async. We
138      know, however, that the messages posted to the worker will
139      be processed in the order they are passed to it, so we can
140      create a queue of callbacks to handle them. The problem
141      with that approach is that it's not error-handling
142      friendly, in that an error can cause us to bypass a result
143      handler queue entry. We have to perform some extra
144      acrobatics to account for that.
146      Problem #2 is that we cannot simply start posting events: we
147      first have to post an 'open' event, wait for it to respond, and
148      collect its db ID before continuing. If we don't wait, we may
149      well fire off 10+ messages before the open actually responds.
150   */
151   const runTests2 = function(){
152     const mustNotReach = ()=>{
153       throw new Error("This is not supposed to be reached.");
154     };
155     runOneTest('exec',{
156       sql: ["create table t(a,b);",
157             "insert into t(a,b) values(1,2),(3,4),(5,6)"
158            ],
159       resultRows: [], columnNames: []
160     }, function(ev){
161       ev = ev.result;
162       T.assert(0===ev.resultRows.length)
163         .assert(0===ev.columnNames.length);
164     });
165     runOneTest('exec',{
166       sql: 'select a a, b b from t order by a',
167       resultRows: [], columnNames: [], saveSql:[]
168     }, function(ev){
169       ev = ev.result;
170       T.assert(3===ev.resultRows.length)
171         .assert(1===ev.resultRows[0][0])
172         .assert(6===ev.resultRows[2][1])
173         .assert(2===ev.columnNames.length)
174         .assert('b'===ev.columnNames[1]);
175     });
176     //if(1){ error("Returning prematurely for testing."); return; }
177     runOneTest('exec',{
178       sql: 'select a a, b b from t order by a',
179       resultRows: [], columnNames: [],
180       rowMode: 'object'
181     }, function(ev){
182       ev = ev.result;
183       T.assert(3===ev.resultRows.length)
184         .assert(1===ev.resultRows[0].a)
185         .assert(6===ev.resultRows[2].b)
186     });
187     runOneTest('exec',{sql:'intentional_error'}, mustNotReach);
188     // Ensure that the message-handler queue survives ^^^ that error...
189     runOneTest('exec',{
190       sql:'select 1',
191       resultRows: [],
192       //rowMode: 'array', // array is the default in the Worker interface
193     }, function(ev){
194       ev = ev.result;
195       T.assert(1 === ev.resultRows.length)
196         .assert(1 === ev.resultRows[0][0]);
197     });
198     runOneTest('exec',{
199       sql: 'select a a, b b from t order by a',
200       callback: 'resultRowTest1',
201       rowMode: 'object'
202     }, function(ev){
203       T.assert(3===dbMsgHandler.resultRowTest1.counter);
204       dbMsgHandler.resultRowTest1.counter = 0;
205     });
206     runOneTest('exec',{
207       sql:[
208         "pragma foreign_keys=0;",
209         // ^^^ arbitrary query with no result columns
210         "select a, b from t order by a desc;",
211         "select a from t;"
212         // exec() only honors SELECT results from the first
213         // statement with result columns (regardless of whether
214         // it has any rows).
215       ],
216       rowMode: 1,
217       resultRows: []
218     },function(ev){
219       const rows = ev.result.resultRows;
220       T.assert(3===rows.length).
221         assert(6===rows[0]);
222     });
223     runOneTest('exec',{sql: 'delete from t where a>3'});
224     runOneTest('exec',{
225       sql: 'select count(a) from t',
226       resultRows: []
227     },function(ev){
228       ev = ev.result;
229       T.assert(1===ev.resultRows.length)
230         .assert(2===ev.resultRows[0][0]);
231     });
232     runOneTest('export',{}, function(ev){
233       ev = ev.result;
234       log("export result:",ev);
235       T.assert('string' === typeof ev.filename)
236         .assert(ev.byteArray instanceof Uint8Array)
237         .assert(ev.byteArray.length > 1024)
238         .assert('application/x-sqlite3' === ev.mimetype);
239     });
240     /***** close() tests must come last. *****/
241     runOneTest('close',{unlink:true},function(ev){
242       ev = ev.result;
243       T.assert('string' === typeof ev.filename);
244     });
245     runOneTest('close',{unlink:true},function(ev){
246       ev = ev.result;
247       T.assert(undefined === ev.filename);
248       logHtml('warning',"This is the final test.");
249     });
250     logHtml('warning',"Finished posting tests. Waiting on async results.");
251   };
253   const runTests = function(){
254     /**
255        Design decision time: all remaining tests depend on the 'open'
256        command having succeeded. In order to support multiple DBs, the
257        upcoming commands ostensibly have to know the ID of the DB they
258        want to talk to. We have two choices:
260        1) We run 'open' and wait for its response, which contains the
261        db id.
263        2) We have the Worker automatically use the current "default
264        db" (the one which was most recently opened) if no db id is
265        provided in the message. When we do this, the main thread may
266        well fire off _all_ of the test messages before the 'open'
267        actually responds, but because the messages are handled on a
268        FIFO basis, those after the initial 'open' will pick up the
269        "default" db. However, if the open fails, then all pending
270        messages (until next next 'open', at least) except for 'close'
271        will fail and we have no way of cancelling them once they've
272        been posted to the worker.
274        Which approach we use below depends on the boolean value of
275        waitForOpen.
276     */
277     const waitForOpen = 1,
278           simulateOpenError = 0 /* if true, the remaining tests will
279                                    all barf if waitForOpen is
280                                    false. */;
281     logHtml('',
282             "Sending 'open' message and",(waitForOpen ? "" : "NOT ")+
283             "waiting for its response before continuing.");
284     startTime = performance.now();
285     runOneTest('open', {
286       filename:'testing2.sqlite3',
287       simulateError: simulateOpenError
288     }, function(ev){
289       log("open result",ev);
290       T.assert('testing2.sqlite3'===ev.result.filename)
291         .assert(ev.dbId)
292         .assert(ev.messageId)
293         .assert('string' === typeof ev.result.vfs);
294       DbState.id = ev.dbId;
295       if(waitForOpen) setTimeout(runTests2, 0);
296     });
297     if(!waitForOpen) runTests2();
298   };
300   SW.onmessage = function(ev){
301     if(!ev.data || 'object'!==typeof ev.data){
302       warn("Unknown sqlite3-worker message type:",ev);
303       return;
304     }
305     ev = ev.data/*expecting a nested object*/;
306     //log("main window onmessage:",ev);
307     if(ev.result && ev.messageId){
308       /* We're expecting a queued-up callback handler. */
309       const f = MsgHandlerQueue.shift();
310       if('error'===ev.type){
311         dbMsgHandler.error(ev);
312         return;
313       }
314       T.assert(f instanceof Function);
315       f(ev);
316       return;
317     }
318     switch(ev.type){
319         case 'sqlite3-api':
320           switch(ev.result){
321               case 'worker1-ready':
322                 log("Message:",ev);
323                 self.sqlite3TestModule.setStatus(null);
324                 runTests();
325                 return;
326               default:
327                 warn("Unknown sqlite3-api message type:",ev);
328                 return;
329           }
330         default:
331           if(dbMsgHandler.hasOwnProperty(ev.type)){
332             try{dbMsgHandler[ev.type](ev);}
333             catch(err){
334               error("Exception while handling db result message",
335                     ev,":",err);
336             }
337             return;
338           }
339           warn("Unknown sqlite3-api message type:",ev);
340     }
341   };
342   log("Init complete, but async init bits may still be running.");
343   log("Installing Worker into global scope SW for dev purposes.");
344   self.SW = SW;
345 })();