1 #include "controllers/controllerengine.h"
2 
3 #include <QScopedPointer>
4 #include <QTemporaryFile>
5 #include <QThread>
6 #include <QtDebug>
7 #include <memory>
8 
9 #include "control/controlobject.h"
10 #include "control/controlpotmeter.h"
11 #include "controllers/controllerdebug.h"
12 #include "controllers/softtakeover.h"
13 #include "preferences/usersettings.h"
14 #include "test/mixxxtest.h"
15 #include "util/color/colorpalette.h"
16 #include "util/time.h"
17 
18 typedef std::unique_ptr<QTemporaryFile> ScopedTemporaryFile;
19 
20 class ControllerEngineTest : public MixxxTest {
21   protected:
makeTemporaryFile(const QString & contents)22     static ScopedTemporaryFile makeTemporaryFile(const QString& contents) {
23         QByteArray contentsBa = contents.toLocal8Bit();
24         ScopedTemporaryFile pFile = std::make_unique<QTemporaryFile>();
25         pFile->open();
26         pFile->write(contentsBa);
27         pFile->close();
28         return pFile;
29     }
30 
SetUp()31     void SetUp() override {
32         mixxx::Time::setTestMode(true);
33         mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10));
34         QThread::currentThread()->setObjectName("Main");
35         cEngine = new ControllerEngine(nullptr, config());
36         pScriptEngine = cEngine->m_pEngine;
37         ControllerDebug::enable();
38         cEngine->setPopups(false);
39     }
40 
TearDown()41     void TearDown() override {
42         cEngine->gracefulShutdown();
43         delete cEngine;
44         mixxx::Time::setTestMode(false);
45     }
46 
execute(const QString & functionName)47     bool execute(const QString& functionName) {
48         QScriptValue function = cEngine->wrapFunctionCode(functionName, 0);
49         return cEngine->internalExecute(QScriptValue(), function,
50                                         QScriptValueList());
51     }
52 
processEvents()53     void processEvents() {
54         // QCoreApplication::processEvents() only processes events that were
55         // queued when the method was called. Hence, all subsequent events that
56         // are emitted while processing those queued events will not be
57         // processed and are enqueued for the next event processing cycle.
58         // Calling processEvents() twice ensures that at least all queued and
59         // the next round of emitted events are processed.
60         application()->processEvents();
61         application()->processEvents();
62     }
63 
64     ControllerEngine *cEngine;
65     QScriptEngine *pScriptEngine;
66 };
67 
TEST_F(ControllerEngineTest,commonScriptHasNoErrors)68 TEST_F(ControllerEngineTest, commonScriptHasNoErrors) {
69     QString commonScript = "./res/controllers/common-controller-scripts.js";
70     cEngine->evaluate(commonScript);
71     EXPECT_FALSE(cEngine->hasErrors(commonScript));
72 }
73 
TEST_F(ControllerEngineTest,setValue)74 TEST_F(ControllerEngineTest, setValue) {
75     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
76     EXPECT_TRUE(execute("function() { engine.setValue('[Test]', 'co', 1.0); }"));
77     EXPECT_DOUBLE_EQ(1.0, co->get());
78 }
79 
TEST_F(ControllerEngineTest,setValue_InvalidControl)80 TEST_F(ControllerEngineTest, setValue_InvalidControl) {
81     EXPECT_TRUE(execute("function() { engine.setValue('[Nothing]', 'nothing', 1.0); }"));
82 }
83 
TEST_F(ControllerEngineTest,getValue_InvalidControl)84 TEST_F(ControllerEngineTest, getValue_InvalidControl) {
85     EXPECT_TRUE(execute("function() { return engine.getValue('[Nothing]', 'nothing'); }"));
86 }
87 
TEST_F(ControllerEngineTest,setValue_IgnoresNaN)88 TEST_F(ControllerEngineTest, setValue_IgnoresNaN) {
89     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
90     co->set(10.0);
91     EXPECT_TRUE(execute("function() { engine.setValue('[Test]', 'co', NaN); }"));
92     EXPECT_DOUBLE_EQ(10.0, co->get());
93 }
94 
95 
TEST_F(ControllerEngineTest,getSetValue)96 TEST_F(ControllerEngineTest, getSetValue) {
97     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
98     EXPECT_TRUE(execute("function() { engine.setValue('[Test]', 'co', engine.getValue('[Test]', 'co') + 1); }"));
99     EXPECT_DOUBLE_EQ(1.0, co->get());
100 }
101 
TEST_F(ControllerEngineTest,setParameter)102 TEST_F(ControllerEngineTest, setParameter) {
103     auto co = std::make_unique<ControlPotmeter>(ConfigKey("[Test]", "co"),
104                                                 -10.0, 10.0);
105     EXPECT_TRUE(execute("function() { engine.setParameter('[Test]', 'co', 1.0); }"));
106     EXPECT_DOUBLE_EQ(10.0, co->get());
107     EXPECT_TRUE(execute("function() { engine.setParameter('[Test]', 'co', 0.0); }"));
108     EXPECT_DOUBLE_EQ(-10.0, co->get());
109     EXPECT_TRUE(execute("function() { engine.setParameter('[Test]', 'co', 0.5); }"));
110     EXPECT_DOUBLE_EQ(0.0, co->get());
111 }
112 
TEST_F(ControllerEngineTest,setParameter_OutOfRange)113 TEST_F(ControllerEngineTest, setParameter_OutOfRange) {
114     auto co = std::make_unique<ControlPotmeter>(ConfigKey("[Test]", "co"),
115                                                 -10.0, 10.0);
116     EXPECT_TRUE(execute("function () { engine.setParameter('[Test]', 'co', 1000); }"));
117     EXPECT_DOUBLE_EQ(10.0, co->get());
118     EXPECT_TRUE(execute("function () { engine.setParameter('[Test]', 'co', -1000); }"));
119     EXPECT_DOUBLE_EQ(-10.0, co->get());
120 }
121 
TEST_F(ControllerEngineTest,setParameter_NaN)122 TEST_F(ControllerEngineTest, setParameter_NaN) {
123     // Test that NaNs are ignored.
124     auto co = std::make_unique<ControlPotmeter>(ConfigKey("[Test]", "co"),
125                                                 -10.0, 10.0);
126     EXPECT_TRUE(execute("function() { engine.setParameter('[Test]', 'co', NaN); }"));
127     EXPECT_DOUBLE_EQ(0.0, co->get());
128 }
129 
TEST_F(ControllerEngineTest,getSetParameter)130 TEST_F(ControllerEngineTest, getSetParameter) {
131     auto co = std::make_unique<ControlPotmeter>(ConfigKey("[Test]", "co"),
132                                                 -10.0, 10.0);
133     EXPECT_TRUE(execute("function() { engine.setParameter('[Test]', 'co', "
134                         "  engine.getParameter('[Test]', 'co') + 0.1); }"));
135     EXPECT_DOUBLE_EQ(2.0, co->get());
136 }
137 
TEST_F(ControllerEngineTest,softTakeover_setValue)138 TEST_F(ControllerEngineTest, softTakeover_setValue) {
139     auto co = std::make_unique<ControlPotmeter>(ConfigKey("[Test]", "co"),
140                                                 -10.0, 10.0);
141     co->setParameter(0.0);
142     EXPECT_TRUE(execute("function() {"
143                         "  engine.softTakeover('[Test]', 'co', true);"
144                         "  engine.setValue('[Test]', 'co', 0.0); }"));
145     // The first set after enabling is always ignored.
146     EXPECT_DOUBLE_EQ(-10.0, co->get());
147 
148     // Change the control internally (putting it out of sync with the
149     // ControllerEngine).
150     co->setParameter(0.5);
151 
152     // Time elapsed is not greater than the threshold, so we do not ignore this
153     // set.
154     EXPECT_TRUE(execute("function() { engine.setValue('[Test]', 'co', -10.0); }"));
155     EXPECT_DOUBLE_EQ(-10.0, co->get());
156 
157     // Advance time to 2x the threshold.
158     mixxx::Time::setTestElapsedTime(SoftTakeover::TestAccess::getTimeThreshold() * 2);
159 
160     // Change the control internally (putting it out of sync with the
161     // ControllerEngine).
162     co->setParameter(0.5);
163 
164     // Ignore the change since it occurred after the threshold and is too large.
165     EXPECT_TRUE(execute("function() { engine.setValue('[Test]', 'co', -10.0); }"));
166     EXPECT_DOUBLE_EQ(0.0, co->get());
167 }
168 
TEST_F(ControllerEngineTest,softTakeover_setParameter)169 TEST_F(ControllerEngineTest, softTakeover_setParameter) {
170     auto co = std::make_unique<ControlPotmeter>(ConfigKey("[Test]", "co"),
171                                                 -10.0, 10.0);
172     co->setParameter(0.0);
173     EXPECT_TRUE(execute("function() {"
174                         "  engine.softTakeover('[Test]', 'co', true);"
175                         "  engine.setParameter('[Test]', 'co', 1.0); }"));
176     // The first set after enabling is always ignored.
177     EXPECT_DOUBLE_EQ(-10.0, co->get());
178 
179     // Change the control internally (putting it out of sync with the
180     // ControllerEngine).
181     co->setParameter(0.5);
182 
183     // Time elapsed is not greater than the threshold, so we do not ignore this
184     // set.
185     EXPECT_TRUE(execute("function() { engine.setParameter('[Test]', 'co', 0.0); }"));
186     EXPECT_DOUBLE_EQ(-10.0, co->get());
187 
188     // Advance time to 2x the threshold.
189     mixxx::Time::setTestElapsedTime(SoftTakeover::TestAccess::getTimeThreshold() * 2);
190 
191     // Change the control internally (putting it out of sync with the
192     // ControllerEngine).
193     co->setParameter(0.5);
194 
195     // Ignore the change since it occurred after the threshold and is too large.
196     EXPECT_TRUE(execute("function() { engine.setParameter('[Test]', 'co', 0.0); }"));
197     EXPECT_DOUBLE_EQ(0.0, co->get());
198 }
199 
TEST_F(ControllerEngineTest,softTakeover_ignoreNextValue)200 TEST_F(ControllerEngineTest, softTakeover_ignoreNextValue) {
201     auto co = std::make_unique<ControlPotmeter>(ConfigKey("[Test]", "co"),
202                                                 -10.0, 10.0);
203     co->setParameter(0.0);
204     EXPECT_TRUE(execute("function() {"
205                         "  engine.softTakeover('[Test]', 'co', true);"
206                         "  engine.setParameter('[Test]', 'co', 1.0); }"));
207     // The first set after enabling is always ignored.
208     EXPECT_DOUBLE_EQ(-10.0, co->get());
209 
210     // Change the control internally (putting it out of sync with the
211     // ControllerEngine).
212     co->setParameter(0.5);
213 
214     EXPECT_TRUE(execute("function() { engine.softTakeoverIgnoreNextValue('[Test]', 'co'); }"));
215 
216     // We would normally allow this set since it is below the time threshold,
217     // but we are ignoring the next value.
218     EXPECT_TRUE(execute("function() { engine.setParameter('[Test]', 'co', 0.0); }"));
219     EXPECT_DOUBLE_EQ(0.0, co->get());
220 }
221 
TEST_F(ControllerEngineTest,reset)222 TEST_F(ControllerEngineTest, reset) {
223     // Test that NaNs are ignored.
224     auto co = std::make_unique<ControlPotmeter>(ConfigKey("[Test]", "co"),
225                                                 -10.0, 10.0);
226     co->setParameter(1.0);
227     EXPECT_TRUE(execute("function() { engine.reset('[Test]', 'co'); }"));
228     EXPECT_DOUBLE_EQ(0.0, co->get());
229 }
230 
TEST_F(ControllerEngineTest,log)231 TEST_F(ControllerEngineTest, log) {
232     EXPECT_TRUE(execute("function() { engine.log('Test that logging works.'); }"));
233 }
234 
TEST_F(ControllerEngineTest,trigger)235 TEST_F(ControllerEngineTest, trigger) {
236     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
237     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
238 
239     ScopedTemporaryFile script(makeTemporaryFile(
240         "var reaction = function(value) { "
241         "  var pass = engine.getValue('[Test]', 'passed');"
242         "  engine.setValue('[Test]', 'passed', pass + 1.0); };"
243         "var connection = engine.connectControl('[Test]', 'co', reaction);"
244         "engine.trigger('[Test]', 'co');"));
245     cEngine->evaluate(script->fileName());
246     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
247     // ControlObjectScript connections are processed via QueuedConnection. Use
248     // processEvents() to cause Qt to deliver them.
249     processEvents();
250     // The counter should have been incremented exactly once.
251     EXPECT_DOUBLE_EQ(1.0, pass->get());
252 }
253 
254 // ControllerEngine::connectControl has a lot of quirky, inconsistent legacy behaviors
255 // depending on how it is invoked, so we need a lot of tests to make sure old scripts
256 // do not break.
257 
TEST_F(ControllerEngineTest,connectControl_ByString)258 TEST_F(ControllerEngineTest, connectControl_ByString) {
259     // Test that connecting and disconnecting by function name works.
260     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
261     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
262 
263     ScopedTemporaryFile script(makeTemporaryFile(
264         "var reaction = function(value) { "
265         "  var pass = engine.getValue('[Test]', 'passed');"
266         "  engine.setValue('[Test]', 'passed', pass + 1.0); };"
267         "engine.connectControl('[Test]', 'co', 'reaction');"
268         "engine.trigger('[Test]', 'co');"
269         "function disconnect() { "
270         "  engine.connectControl('[Test]', 'co', 'reaction', 1);"
271         "  engine.trigger('[Test]', 'co'); }"));
272 
273     cEngine->evaluate(script->fileName());
274     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
275     // ControlObjectScript connections are processed via QueuedConnection. Use
276     // processEvents() to cause Qt to deliver them.
277     processEvents();
278     EXPECT_TRUE(execute("disconnect"));
279     processEvents();
280     // The counter should have been incremented exactly once.
281     EXPECT_DOUBLE_EQ(1.0, pass->get());
282 }
283 
TEST_F(ControllerEngineTest,connectControl_ByStringForbidDuplicateConnections)284 TEST_F(ControllerEngineTest, connectControl_ByStringForbidDuplicateConnections) {
285     // Test that connecting a control to a callback specified by a string
286     // does not make duplicate connections. This behavior is inconsistent
287     // with the behavior when specifying a callback as a function, but
288     // this is how it has been done, so keep the behavior to ensure old scripts
289     // do not break.
290     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
291     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
292 
293     ScopedTemporaryFile script(makeTemporaryFile(
294         "var reaction = function(value) { "
295         "  var pass = engine.getValue('[Test]', 'passed');"
296         "  engine.setValue('[Test]', 'passed', pass + 1.0); };"
297         "engine.connectControl('[Test]', 'co', 'reaction');"
298         "engine.connectControl('[Test]', 'co', 'reaction');"
299         "engine.trigger('[Test]', 'co');"));
300 
301     cEngine->evaluate(script->fileName());
302     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
303     // ControlObjectScript connections are processed via QueuedConnection. Use
304     // processEvents() to cause Qt to deliver them.
305     processEvents();
306     // The counter should have been incremented exactly once.
307     EXPECT_DOUBLE_EQ(1.0, pass->get());
308 }
309 
TEST_F(ControllerEngineTest,connectControl_ByStringRedundantConnectionObjectsAreNotIndependent)310 TEST_F(ControllerEngineTest,
311        connectControl_ByStringRedundantConnectionObjectsAreNotIndependent) {
312     // Test that multiple connections are not allowed when passing
313     // the callback to engine.connectControl as a function name string.
314     // This is weird and inconsistent, but it is how it has been done,
315     // so keep this behavior to make sure old scripts do not break.
316     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
317     auto counter = std::make_unique<ControlObject>(ConfigKey("[Test]", "counter"));
318 
319     ScopedTemporaryFile script(makeTemporaryFile(
320         "var incrementCounterCO = function () {"
321         "  var counter = engine.getValue('[Test]', 'counter');"
322         "  engine.setValue('[Test]', 'counter', counter + 1);"
323         "};"
324         "var connection1 = engine.connectControl('[Test]', 'co', 'incrementCounterCO');"
325         // Make a second connection with the same ControlObject
326         // to check that disconnecting one does not disconnect both.
327         "var connection2 = engine.connectControl('[Test]', 'co', 'incrementCounterCO');"
328         "function changeTestCoValue() {"
329         "  var testCoValue = engine.getValue('[Test]', 'co');"
330         "  engine.setValue('[Test]', 'co', testCoValue + 1);"
331         "};"
332         "function disconnectConnection2() {"
333         "  connection2.disconnect();"
334         "};"
335     ));
336 
337     cEngine->evaluate(script->fileName());
338     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
339     execute("changeTestCoValue");
340     // ControlObjectScript connections are processed via QueuedConnection. Use
341     // processEvents() to cause Qt to deliver them.
342     processEvents();
343     EXPECT_EQ(1.0, counter->get());
344 
345     execute("disconnectConnection2");
346     // The connection objects should refer to the same connection,
347     // so disconnecting one should disconnect both.
348     execute("changeTestCoValue");
349     processEvents();
350     EXPECT_EQ(1.0, counter->get());
351 }
352 
TEST_F(ControllerEngineTest,connectControl_ByFunction)353 TEST_F(ControllerEngineTest, connectControl_ByFunction) {
354     // Test that connecting and disconnecting with a function value works.
355     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
356     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
357 
358     ScopedTemporaryFile script(makeTemporaryFile(
359         "var reaction = function(value) { "
360         "  var pass = engine.getValue('[Test]', 'passed');"
361         "  engine.setValue('[Test]', 'passed', pass + 1.0); };"
362         "var connection = engine.connectControl('[Test]', 'co', reaction);"
363         "connection.trigger();"));
364 
365     cEngine->evaluate(script->fileName());
366     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
367     // ControlObjectScript connections are processed via QueuedConnection. Use
368     // processEvents() to cause Qt to deliver them.
369     processEvents();
370     // The counter should have been incremented exactly once.
371     EXPECT_DOUBLE_EQ(1.0, pass->get());
372 }
373 
TEST_F(ControllerEngineTest,connectControl_ByFunctionAllowDuplicateConnections)374 TEST_F(ControllerEngineTest, connectControl_ByFunctionAllowDuplicateConnections) {
375     // Test that duplicate connections are allowed when passing callbacks as functions.
376     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
377     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
378 
379     ScopedTemporaryFile script(makeTemporaryFile(
380         "var reaction = function(value) { "
381         "  var pass = engine.getValue('[Test]', 'passed');"
382         "  engine.setValue('[Test]', 'passed', pass + 1.0); };"
383         "engine.connectControl('[Test]', 'co', reaction);"
384         "engine.connectControl('[Test]', 'co', reaction);"
385         // engine.trigger() has no way to know which connection to a ControlObject
386         // to trigger, so it should trigger all of them.
387         "engine.trigger('[Test]', 'co');"));
388 
389     cEngine->evaluate(script->fileName());
390     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
391     // ControlObjectScript connections are processed via QueuedConnection. Use
392     // processEvents() to cause Qt to deliver them.
393     processEvents();
394     // The counter should have been incremented exactly twice.
395     EXPECT_DOUBLE_EQ(2.0, pass->get());
396 }
397 
TEST_F(ControllerEngineTest,connectControl_toDisconnectRemovesAllConnections)398 TEST_F(ControllerEngineTest, connectControl_toDisconnectRemovesAllConnections) {
399     // Test that every connection to a ControlObject is disconnected
400     // by calling engine.connectControl(..., true). Individual connections
401     // can only be disconnected by storing the connection object returned by
402     // engine.connectControl and calling that object's 'disconnect' method.
403     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
404     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
405 
406     ScopedTemporaryFile script(makeTemporaryFile(
407         "var reaction = function(value) { "
408         "  var pass = engine.getValue('[Test]', 'passed');"
409         "  engine.setValue('[Test]', 'passed', pass + 1.0); };"
410         "engine.connectControl('[Test]', 'co', reaction);"
411         "engine.connectControl('[Test]', 'co', reaction);"
412         "engine.trigger('[Test]', 'co');"
413         "function disconnect() { "
414         "  engine.connectControl('[Test]', 'co', reaction, 1);"
415         "  engine.trigger('[Test]', 'co'); }"));
416 
417     cEngine->evaluate(script->fileName());
418     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
419     // ControlObjectScript connections are processed via QueuedConnection. Use
420     // processEvents() to cause Qt to deliver them.
421     processEvents();
422     EXPECT_TRUE(execute("disconnect"));
423     processEvents();
424     // The counter should have been incremented exactly twice.
425     EXPECT_DOUBLE_EQ(2.0, pass->get());
426 }
427 
TEST_F(ControllerEngineTest,connectControl_ByLambda)428 TEST_F(ControllerEngineTest, connectControl_ByLambda) {
429     // Test that connecting with an anonymous function works.
430     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
431     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
432 
433     ScopedTemporaryFile script(makeTemporaryFile(
434         "var connection = engine.connectControl('[Test]', 'co', function(value) { "
435         "  var pass = engine.getValue('[Test]', 'passed');"
436         "  engine.setValue('[Test]', 'passed', pass + 1.0); });"
437         "connection.trigger();"
438         "function disconnect() { "
439         "  connection.disconnect();"
440         "  engine.trigger('[Test]', 'co'); }"));
441 
442     cEngine->evaluate(script->fileName());
443     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
444     // ControlObjectScript connections are processed via QueuedConnection. Use
445     // processEvents() to cause Qt to deliver them.
446     processEvents();
447     EXPECT_TRUE(execute("disconnect"));
448     processEvents();
449     // The counter should have been incremented exactly once.
450     EXPECT_DOUBLE_EQ(1.0, pass->get());
451 }
452 
TEST_F(ControllerEngineTest,connectionObject_Disconnect)453 TEST_F(ControllerEngineTest, connectionObject_Disconnect) {
454     // Test that disconnecting using the 'disconnect' method on the connection
455     // object returned from connectControl works.
456     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
457     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
458 
459     ScopedTemporaryFile script(makeTemporaryFile(
460         "var reaction = function(value) { "
461         "  var pass = engine.getValue('[Test]', 'passed');"
462         "  engine.setValue('[Test]', 'passed', pass + 1.0); };"
463         "var connection = engine.makeConnection('[Test]', 'co', reaction);"
464         "connection.trigger();"
465         "function disconnect() { "
466         "  connection.disconnect();"
467         "  engine.trigger('[Test]', 'co'); }"));
468 
469     cEngine->evaluate(script->fileName());
470     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
471     // ControlObjectScript connections are processed via QueuedConnection. Use
472     // processEvents() to cause Qt to deliver them.
473     processEvents();
474     EXPECT_TRUE(execute("disconnect"));
475     processEvents();
476     // The counter should have been incremented exactly once.
477     EXPECT_DOUBLE_EQ(1.0, pass->get());
478 }
TEST_F(ControllerEngineTest,connectionObject_reflectDisconnect)479 TEST_F(ControllerEngineTest, connectionObject_reflectDisconnect) {
480     // Test that checks if disconnecting yields the appropriate feedback
481     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
482     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
483 
484     ScopedTemporaryFile script(makeTemporaryFile(
485         "var reaction = function(success) { "
486         "  if (success) {"
487         "    var pass = engine.getValue('[Test]', 'passed');"
488         "    engine.setValue('[Test]', 'passed', pass + 1.0); "
489         "  }"
490         "};"
491         "var dummy_callback = function(value) {};"
492         "var connection = engine.makeConnection('[Test]', 'co', dummy_callback);"
493         "reaction(connection);"
494         "reaction(connection.isConnected);"
495         "var successful_disconnect = connection.disconnect();"
496         "reaction(successful_disconnect);"
497         "reaction(!connection.isConnected);"
498     ));
499     cEngine->evaluate(script->fileName());
500     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
501     processEvents();
502     EXPECT_DOUBLE_EQ(4.0, pass->get());
503 }
504 
505 
TEST_F(ControllerEngineTest,connectionObject_DisconnectByPassingToConnectControl)506 TEST_F(ControllerEngineTest, connectionObject_DisconnectByPassingToConnectControl) {
507     // Test that passing a connection object back to engine.connectControl
508     // removes the connection
509     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
510     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
511     // The connections should be removed from the ControlObject which they were
512     // actually connected to, regardless of the group and item arguments passed
513     // to engine.connectControl() to remove the connection. All that should matter
514     // is that a valid ControlObject is specified.
515     auto dummy = std::make_unique<ControlObject>(ConfigKey("[Test]", "dummy"));
516 
517     ScopedTemporaryFile script(makeTemporaryFile(
518         "var reaction = function(value) { "
519         "  var pass = engine.getValue('[Test]', 'passed');"
520         "  engine.setValue('[Test]', 'passed', pass + 1.0); };"
521         "var connection1 = engine.connectControl('[Test]', 'co', reaction);"
522         "var connection2 = engine.connectControl('[Test]', 'co', reaction);"
523         "function disconnectConnection1() { "
524         "  engine.connectControl('[Test]',"
525         "                        'dummy',"
526         "                        connection1);"
527         "  engine.trigger('[Test]', 'co'); }"
528         // Whether a 4th argument is passed to engine.connectControl does not matter.
529         "function disconnectConnection2() { "
530         "  engine.connectControl('[Test]',"
531         "                        'dummy',"
532         "                        connection2, true);"
533         "  engine.trigger('[Test]', 'co'); }"));
534 
535     cEngine->evaluate(script->fileName());
536     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
537     // ControlObjectScript connections are processed via QueuedConnection. Use
538     // processEvents() to cause Qt to deliver them.
539     processEvents();
540     EXPECT_TRUE(execute("disconnectConnection1"));
541     processEvents();
542     // The counter should have been incremented once by connection2.
543     EXPECT_DOUBLE_EQ(1.0, pass->get());
544     EXPECT_TRUE(execute("disconnectConnection2"));
545     processEvents();
546     // The counter should not have changed.
547     EXPECT_DOUBLE_EQ(1.0, pass->get());
548 }
549 
TEST_F(ControllerEngineTest,connectionObject_MakesIndependentConnection)550 TEST_F(ControllerEngineTest, connectionObject_MakesIndependentConnection) {
551     // Test that multiple connections can be made to the same CO with
552     // the same callback function and that calling their 'disconnect' method
553     // only disconnects the callback for that object.
554     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
555     auto counter = std::make_unique<ControlObject>(ConfigKey("[Test]", "counter"));
556 
557     ScopedTemporaryFile script(makeTemporaryFile(
558         "var incrementCounterCO = function () {"
559         "  var counter = engine.getValue('[Test]', 'counter');"
560         "  engine.setValue('[Test]', 'counter', counter + 1);"
561         "};"
562         "var connection1 = engine.makeConnection('[Test]', 'co', incrementCounterCO);"
563         // Make a second connection with the same ControlObject
564         // to check that disconnecting one does not disconnect both.
565         "var connection2 = engine.makeConnection('[Test]', 'co', incrementCounterCO);"
566         "function changeTestCoValue() {"
567         "  var testCoValue = engine.getValue('[Test]', 'co');"
568         "  engine.setValue('[Test]', 'co', testCoValue + 1);"
569         "}"
570         "function disconnectConnection1() {"
571         "  connection1.disconnect();"
572         "}"
573     ));
574 
575     cEngine->evaluate(script->fileName());
576     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
577     execute("changeTestCoValue");
578     // ControlObjectScript connections are processed via QueuedConnection. Use
579     // processEvents() to cause Qt to deliver them.
580     processEvents();
581     EXPECT_EQ(2.0, counter->get());
582 
583     execute("disconnectConnection1");
584     // Only the callback for connection1 should have disconnected;
585     // the callback for connection2 should still be connected, so
586     // changing the CO they were both connected to should
587     // increment the counter once.
588     execute("changeTestCoValue");
589     processEvents();
590     EXPECT_EQ(3.0, counter->get());
591 }
592 
TEST_F(ControllerEngineTest,connectionObject_trigger)593 TEST_F(ControllerEngineTest, connectionObject_trigger) {
594     // Test that triggering using the 'trigger' method on the connection
595     // object returned from connectControl works.
596     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
597     auto counter = std::make_unique<ControlObject>(ConfigKey("[Test]", "counter"));
598 
599     ScopedTemporaryFile script(makeTemporaryFile(
600         "var incrementCounterCO = function () {"
601         "  var counter = engine.getValue('[Test]', 'counter');"
602         "  engine.setValue('[Test]', 'counter', counter + 1);"
603         "};"
604         "var connection1 = engine.makeConnection('[Test]', 'co', incrementCounterCO);"
605         // Make a second connection with the same ControlObject
606         // to check that triggering a connection object only triggers that callback,
607         // not every callback connected to its ControlObject.
608         "var connection2 = engine.makeConnection('[Test]', 'co', incrementCounterCO);"
609         "connection1.trigger();"
610     ));
611 
612     cEngine->evaluate(script->fileName());
613     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
614     // The counter should have been incremented exactly once.
615     EXPECT_DOUBLE_EQ(1.0, counter->get());
616 }
617 
618 
TEST_F(ControllerEngineTest,connectionExecutesWithCorrectThisObject)619 TEST_F(ControllerEngineTest, connectionExecutesWithCorrectThisObject) {
620     // Test that callback functions are executed with JavaScript's
621     // 'this' keyword referring to the object in which the connection
622     // was created.
623     auto co = std::make_unique<ControlObject>(ConfigKey("[Test]", "co"));
624     auto pass = std::make_unique<ControlObject>(ConfigKey("[Test]", "passed"));
625 
626     ScopedTemporaryFile script(makeTemporaryFile(
627         "var TestObject = function () {"
628         "  this.executeTheCallback = true;"
629         "  this.connection = engine.makeConnection('[Test]', 'co', function () {"
630         "    if (this.executeTheCallback) {"
631         "      engine.setValue('[Test]', 'passed', 1);"
632         "    }"
633         "  });"
634         "};"
635         "var someObject = new TestObject();"
636         "someObject.connection.trigger();"));
637 
638     cEngine->evaluate(script->fileName());
639     EXPECT_FALSE(cEngine->hasErrors(script->fileName()));
640     // ControlObjectScript connections are processed via QueuedConnection. Use
641     // processEvents() to cause Qt to deliver them.
642     processEvents();
643     // The counter should have been incremented exactly once.
644     EXPECT_DOUBLE_EQ(1.0, pass->get());
645 }
646