• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 33466651246

01 Sep 2026 03:33AM UTC coverage: 23.689% (+0.1%) from 23.563%
33466651246

push

github

jcelerier
tests: a latched-out exit reports that closing may proceed

RegressionDoubleExitTest asserted that a second Presenter::exit() returns false,
and its header documented that as the contract: "any further call while exiting
(or after a completed exit) is a no-op returning false".

That return is also what View::closeEvent turns into accept/ignore:

    if(m_presenter->exit()) ev->accept(); else ev->ignore();

and since Qt 6.6 QCoreApplication::quit() closes the top-level windows and
honours the refusal. So false made the latch veto the very quit that
forceExit() had just scheduled, and no windowed instance could be shut down over
OSC /exit -- the defect fixed in fe3fd08a64, which changed exit() to answer true
and left this test asserting the behaviour it had just removed. Master has been
red since that merge; I opened it without running this test.

The property the test is named for is unchanged and still asserted: a second
request is a no-op that does not re-enter closeAllDocuments. Only the answer it
gives the caller changes, from "refuse the close" to "closing may proceed".

Negative-controlled: with `== false` restored, 2 of 4 assertions fail; with
`== true`, 4/4 pass.

53523 of 225936 relevant lines covered (23.69%)

62475.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.88
/src/plugins/score-plugin-js/JS/ApplicationPlugin.cpp
1
#include "ApplicationPlugin.hpp"
2

3
#include <JS/DocumentPlugin.hpp>
4
#include <JS/Qml/DeviceContext.hpp>
5
#include <JS/Qml/EditContext.hpp>
6
#include <JS/Qml/Utils.hpp>
7
#include <JS/Qml/ViewContext.hpp>
8
#include <Library/LibrarySettings.hpp>
9
#include <LocalTree/LocalTreeDocumentPlugin.hpp>
10

11
#include <core/application/ApplicationInterface.hpp>
12
#include <core/document/Document.hpp>
13
#include <core/presenter/DocumentManager.hpp>
14

15
#include <ossia/detail/thread.hpp>
16

17
#include <ossia-qt/invoke.hpp>
18
#include <ossia-qt/qml_protocols.hpp>
19

20
#include <QCommandLineParser>
21
#include <QFile>
22
#include <QFileInfo>
23

24
#if __has_include(<QQuickWindow>)
25
#include <QGuiApplication>
26
#include <QQuickItem>
27
#include <QQuickWindow>
28
#endif
29

30
#if SCORE_HAS_GPU_JS
31
#include <Gfx/Settings/Model.hpp>
32
#endif
33

34
#include <ossia/network/context.hpp>
35

36
namespace JS
37
{
38
// Whether --script was given a program or the path of one. An existing file is
39
// always a path; anything with JS punctuation in it is a program.
40
static bool stringIsScript(const QString& input)
13✔
41
{
42
  if(input.isEmpty())
13✔
43
    return false;
×
44

45
  if(QFileInfo fileInfo{input}; fileInfo.exists() && fileInfo.isFile())
22✔
46
    return false;
9✔
47

48
  if(input.length() > 4096)
4✔
49
    return true;
×
50

51
  for(QChar ch : input)
56✔
52
  {
53
    const char16_t c = ch.unicode();
55✔
54
    if(c == '\n' || c == '\r' || c == ';' || c == '{' || c == '}' || c == '('
55✔
55
       || c == ')')
55✔
56
      return true;
3✔
57
  }
58

59
  return false;
1✔
60
}
13✔
61

62
ApplicationPlugin::ApplicationPlugin(const score::GUIApplicationContext& ctx)
267✔
63
    : score::GUIApplicationPlugin{ctx}
267✔
64
{
267✔
65
#if __has_include(<QQuickWindow>)
66
  // Crisp text in every QML UI: distance-field rendering looks blurry at the small
67
  // font sizes our panels use, native glyph rendering matches the rest of score.
68
  QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
267✔
69
#endif
70

71
  // For the console
72
  m_consoleEngine.globalObject().setProperty("Score", m_consoleEngine.newQObject(new EditJsContext));
267✔
73
  m_consoleEngine.globalObject().setProperty("Util", m_consoleEngine.newQObject(new JsUtils));
267✔
74
  m_consoleEngine.globalObject().setProperty(
534✔
75
      "System", m_consoleEngine.newQObject(new JsSystem));
267✔
76
  m_consoleEngine.globalObject().setProperty(
534✔
77
      "Library", m_consoleEngine.newQObject(new JsLibrary));
267✔
78
  m_consoleEngine.globalObject().setProperty("Device", m_consoleEngine.newQObject(new DeviceContext{m_consoleEngine}));
267✔
79
  m_consoleEngine.globalObject().setProperty("View", m_consoleEngine.newQObject(new JsViewContext));
267✔
80
  connect(&m_consoleEngine, &QQmlEngine::exit, this, [&](int retCode) {
273✔
81
    for(auto& doc : score::GUIAppContext().docManager.documents())
12✔
82
      doc->commandStack().markCurrentIndexAsSaved();
6✔
83
    // quit() is exit(0), which discarded the code Qt.exit() was given: a script
84
    // could stop the app but never report that it had failed.
85
    qApp->exit(retCode);
6✔
86
    QTimer::singleShot(
6✔
87
        500, [] { score::GUIApplicationInterface::instance().forceExit(); });
×
88
  });
6✔
89
  m_asioContext = std::make_shared<ossia::net::network_context>();
267✔
90
  m_processMessages = true;
267✔
91
  m_consoleEngine.globalObject().setProperty(
534✔
92
      "Protocols",
267✔
93
      m_consoleEngine.newQObject(new ossia::qt::qml_protocols{m_asioContext, this}));
267✔
94
  m_asioThread = std::thread{[this] {
534✔
95
    ossia::set_thread_name("ossia app asio");
267✔
96
    while(m_processMessages)
534✔
97
    {
98
      m_asioContext->run();
267✔
99
    }
100
  }};
267✔
101

102
  // For scripts of processes that run in the ui thread:
103
  m_scriptProcessUIEngine.globalObject().setProperty(
534✔
104
      "Util", m_scriptProcessUIEngine.newQObject(new JsUtils));
267✔
105
  m_scriptProcessUIEngine.globalObject().setProperty(
534✔
106
      "System", m_scriptProcessUIEngine.newQObject(new JsSystem));
267✔
107
  m_scriptProcessUIEngine.globalObject().setProperty(
534✔
108
      "Library", m_scriptProcessUIEngine.newQObject(new JsLibrary));
267✔
109
  m_scriptProcessUIEngine.globalObject().setProperty(
534✔
110
      "View", m_scriptProcessUIEngine.newQObject(new JsViewContext));
267✔
111

112
  // Command-line option parsing
113
  QCommandLineParser parser;
267✔
114

115
  QCommandLineOption script_opt(
267✔
116
      "script", QCoreApplication::translate("js", "script"), "Script", "");
267✔
117
  parser.addOption(script_opt);
267✔
118

119
  parser.parse(ctx.applicationSettings.arguments);
267✔
120
  for(const QString& script : parser.values(script_opt))
280✔
121
  {
122
    if(script.isEmpty())
13✔
123
      continue;
×
124

125
    if(stringIsScript(script))
13✔
126
    {
127
      this->m_start_scripts.push_back(StartScript{.source = script});
3✔
128
      continue;
129
    }
130

131
    QFile f{script};
10✔
132
    if(!f.open(QIODevice::ReadOnly))
10✔
133
    {
134
      qCritical() << "--script: cannot open" << script << ":" << f.errorString();
1✔
135
      this->m_start_script_failed = true;
1✔
136
      continue;
1✔
137
    }
138

139
    const QFileInfo fi{f};
9✔
140
    StartScript s;
9✔
141
    s.name = script;
9✔
142
    s.file = fi.canonicalFilePath();
9✔
143
    s.dir = fi.canonicalPath();
9✔
144
    // .mjs is what the rest of score calls an ES module (see the library's
145
    // ModuleLibraryHandler); only those go through importModule().
146
    s.module = fi.suffix().compare(QStringLiteral("mjs"), Qt::CaseInsensitive) == 0;
9✔
147
    if(!s.module)
9✔
148
      s.source = QString::fromUtf8(f.readAll());
5✔
149

150
    this->m_start_scripts.push_back(std::move(s));
9✔
151
  }
10✔
152
}
×
153

154
QJSValue ApplicationPlugin::importModule(QQmlEngine& engine, const QString& path)
4✔
155
{
156
  QJSValue mod = engine.importModule(path);
4✔
157
  if(mod.isError())
4✔
158
    return mod;
1✔
159

160
  if(auto init = mod.property("initialize"); init.isCallable())
5✔
161
    if(const auto res = init.call(); res.isError())
4✔
162
      return res;
1✔
163

164
  engine.globalObject().setProperty(QFileInfo{path}.baseName(), mod);
2✔
165
  return mod;
2✔
166
}
4✔
167

168
void ApplicationPlugin::on_newDocument(score::Document& doc)
157✔
169
{
170
  score::addDocumentPlugin<DocumentPlugin>(doc);
157✔
171
}
157✔
172

173
ApplicationPlugin::~ApplicationPlugin()
532✔
174
{
266✔
175
  m_processMessages = false;
266✔
176
  m_asioContext->context.stop();
266✔
177
  m_asioThread.join();
266✔
178
}
532✔
179

180
void ApplicationPlugin::on_createdDocument(score::Document& doc)
166✔
181
{
182
  // Local Tree
183
  LocalTree::DocumentPlugin* lt = doc.context().findPlugin<LocalTree::DocumentPlugin>();
166✔
184
  if(lt)
166✔
185
  {
186
    auto& root = lt->device().get_root_node();
166✔
187

188
    auto node = root.create_child("script");
166✔
189
    auto address = node->create_parameter(ossia::val_type::STRING);
166✔
190
    address->set_value(std::string{});
166✔
191
    address->set_access(ossia::access_mode::SET);
166✔
192
    address->add_callback([&](const ossia::value& v) {
166✔
193
      ossia::qt::run_async(
×
194
          this, [this, str = QString::fromStdString(ossia::convert<std::string>(v))] {
×
195
        auto res = m_consoleEngine.evaluate(str);
×
196
        if(res.isError())
×
197
        {
198
          qDebug() << res.toString();
×
199
        }
×
200
      });
×
201
    });
×
202
  }
166✔
203

204
  // Custom data
205
  if(auto customData = doc.context().findPlugin<DocumentPlugin>(); !customData)
166✔
206
    score::addDocumentPlugin<DocumentPlugin>(doc);
×
207

208
  if(m_start_script_failed)
166✔
209
  {
210
    qGuiApp->exit(2);
1✔
211
    return;
1✔
212
  }
213

214
  if(!m_start_scripts.empty())
165✔
215
  {
216
    QTimer::singleShot(100, this, [this] {
20✔
217
      for(const StartScript& s : m_start_scripts)
17✔
218
      {
219
        if(!s.dir.isEmpty())
11✔
220
          m_consoleEngine.addImportPath(s.dir);
9✔
221

222
        // Report a throwing --script: an unresolvable readFile returns an empty
223
        // string and eval("") is a no-op, so the process would otherwise exit
224
        // reporting success and a harness could not tell that from a pass.
225
        const auto res = s.module ? importModule(m_consoleEngine, s.file)
11✔
226
                                  : m_consoleEngine.evaluate(s.source, s.name);
7✔
227
        if(res.isError())
11✔
228
        {
229
          qCritical().noquote()
8✔
230
              << "--script:"
4✔
231
              << (s.name.isEmpty() ? QStringLiteral("<inline>") : s.name) << "line"
4✔
232
              << res.property("lineNumber").toInt() << ":" << res.toString();
4✔
233
          qGuiApp->exit(3);
4✔
234
          return;
4✔
235
        }
236
      }
11✔
237
    });
10✔
238
  }
10✔
239
}
166✔
240
void ApplicationPlugin::afterStartup()
11✔
241
{
242
  // Dummy engine setup for JS processes
243
  // eng.importModule(
244
  //     "/home/jcelerier/Documents/ossia/score/packages/default/Scripts/include/"
245
  //     "tonal.mjs");
246
  for(auto& p : this->context.settings<Library::Settings::Model>().getIncludePaths())
11✔
247
  {
248
    m_scriptProcessUIEngine.addImportPath(p);
×
249
    // The console engine runs --script, the console panel and every library
250
    // .mjs; without this they could not import what a JS process can, which
251
    // made the same `import` line work in a process and fail in a script.
252
    m_consoleEngine.addImportPath(p);
×
253
  }
254

255
#if __has_include(<QQuickWindow>)
256
  if(QFileInfo f{context.applicationSettings.ui}; f.isFile())
11✔
257
  {
258
    m_comp = new QQmlComponent{&m_consoleEngine, f.absoluteFilePath(), this};
×
259

260
    if(auto obj = m_comp->create())
×
261
    {
262
      if(auto item = qobject_cast<QQuickItem*>(obj))
×
263
      {
264
        m_window = new QQuickWindow{};
×
265
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
266
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
267
  // leaves the window with no title bar, close or minimise button.
268
  m_window->setFlags(
×
269
      m_window->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
×
270
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
×
271
      | Qt::WindowMaximizeButtonHint);
×
272
#if defined(__EMSCRIPTEN__)
273
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
274
  // turns into showFullScreen() for every top level: the requested size is
275
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
276
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
277
  m_window->setFlags(m_window->flags() | Qt::Dialog);
278
#endif
279
        m_window->setWidth(640);
×
280
        m_window->setHeight(480);
×
281
        item->setParentItem(m_window->contentItem());
×
282
#if defined(__EMSCRIPTEN__)
283
        m_window->showNormal();
284
#else
285
        m_window->show();
×
286
#endif
287
        return;
×
288
      }
289
    }
×
290
    else
291
    {
292
      qDebug() << m_comp->errorString();
×
293
      qGuiApp->exit(1);
×
294
    }
295
    delete m_comp;
×
296
  }
×
297
#endif
298
}
11✔
299
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc