File size: 5,915 Bytes
30c32c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
const fs = require('fs');
const path = require('path');

const test = require('tap').test;

const log = require('../../src/util/log');
const makeTestStorage = require('../fixtures/make-test-storage');
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
const VirtualMachine = require('../../src/index');

/**
 * @fileoverview Transform each sb2 in fixtures/execute into a test.
 *
 * Test execution of a group of scratch blocks by SAYing if a test did "pass",
 * or did "fail". Four keywords can be set at the beginning of a SAY messaage
 * to indicate a test primitive.
 *
 * - "pass MESSAGE" will t.pass(MESSAGE).
 * - "fail MESSAGE" will t.fail(MESSAGE).
 * - "plan NUMBER_OF_TESTS" will t.plan(Number(NUMBER_OF_TESTS)).
 * - "end" will t.end().
 *
 * A good strategy to follow is to SAY "plan NUMBER_OF_TESTS" first. Then
 * "pass" and "fail" depending on expected scratch results in conditions, event
 * scripts, or what is best for testing the target block or group of blocks.
 * When its done you must SAY "end" so the test and tap know that the end has
 * been reached.
 */

const whenThreadsComplete = (t, vm, timeLimit = 2000) => (
    // When the number of threads reaches 0 the test is expected to be complete.
    new Promise((resolve, reject) => {
        const intervalId = setInterval(() => {
            let active = 0;
            const threads = vm.runtime.threads;
            for (let i = 0; i < threads.length; i++) {
                if (!threads[i].updateMonitor) {
                    active += 1;
                }
            }
            if (active === 0) {
                resolve();
            }
        }, 50);

        const timeoutId = setTimeout(() => {
            reject(new Error('time limit reached'));
        }, timeLimit);

        // Clear the interval to allow the process to exit
        // naturally.
        t.tearDown(() => {
            clearInterval(intervalId);
            clearTimeout(timeoutId);
        });
    })
);

const executeDir = path.resolve(__dirname, '../fixtures/execute');

fs.readdirSync(executeDir)
    .filter(uri => uri.endsWith('.sb2') || uri.endsWith('.sb3'))
    .forEach(uri => {
        const run = (t, enableCompiler) => {
            // Disable logging during this test.
            log.suggest.deny('vm', 'error');
            t.tearDown(() => log.suggest.clear());

            // Map string messages to tap reporting methods. This will be used
            // with events from scratch's runtime emitted on block instructions.
            let didPlan;
            let didEnd;
            const reporters = {
                comment (message) {
                    t.comment(message);
                },
                pass (reason) {
                    t.pass(reason);
                },
                fail (reason) {
                    t.fail(reason);
                },
                plan (count) {
                    didPlan = true;
                    t.plan(Number(count));
                },
                end () {
                    didEnd = true;
                    t.end();
                }
            };
            const reportVmResult = text => {
                const command = text.split(/\s+/, 1)[0].toLowerCase();
                if (reporters[command]) {
                    return reporters[command](text.substring(command.length).trim());
                }

                // Default to a comment with the full text if we didn't match
                // any command prefix
                return reporters.comment(text);
            };

            const vm = new VirtualMachine();
            vm.attachStorage(makeTestStorage());

            // Start the VM and initialize some vm properties.
            // complete.
            vm.start();
            vm.clear();
            vm.setCompatibilityMode(false);
            vm.setTurboMode(false);
            vm.setCompilerOptions({enabled: enableCompiler});

            // TW: Script compilation errors should fail.
            if (enableCompiler) {
                vm.on('COMPILE_ERROR', (target, error) => {
                    // Edge-activated hats are a known error.
                    if (!`${error}`.includes('edge-activated hat')) {
                        throw new Error(`Could not compile script in ${target.getName()}: ${error}`);
                    }
                });
            }

            // Stop the runtime interval once the test is complete so the test
            // process may naturally exit.
            t.tearDown(() => {
                vm.stop();
            });

            // Report the text of SAY events as testing instructions.
            vm.runtime.on('SAY', (target, type, text) => reportVmResult(text));

            const project = readFileToBuffer(path.resolve(executeDir, uri));

            // Load the project and once all threads are complete ensure that
            // the scratch project sent us a "end" message.
            return vm.loadProject(project)
                .then(() => vm.greenFlag())
                .then(() => whenThreadsComplete(t, vm))
                .then(() => {
                    // Setting a plan is not required but is a good idea.
                    if (!didPlan) {
                        t.comment('did not say "plan NUMBER_OF_TESTS"');
                    }

                    // End must be called so that tap knows the test is done. If
                    // the test has an SAY "end" block but that block did not
                    // execute, this explicit failure will raise that issue so
                    // it can be resolved.
                    if (!didEnd) {
                        t.fail('did not say "end"');
                        t.end();
                    }
                });
        };
        test(`${uri} (interpreted)`, t => run(t, false));
        test(`${uri} (compiled)`, t => run(t, true));
    });