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

ossia / score / 36105248298

25 Sep 2026 06:57AM UTC coverage: 42.611% (+0.4%) from 42.23%
36105248298

Pull #2308

github

web-flow
Merge 07da0e711 into c0c64ec39
Pull Request #2308: tests: a green test suite, and the sanitizer findings in score fixed

511 of 687 new or added lines in 50 files covered. (74.38%)

184 existing lines in 23 files now uncovered.

127081 of 298233 relevant lines covered (42.61%)

118531.04 hits per line

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

89.09
/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
#include <QString>
24

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

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

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

37
namespace JS
38
{
39
// Check whether the input is a script, or a file path.
40
// An existing file always wins: a real path may legitimately contain
41
// characters (parentheses, braces, ...) that also occur in inline source,
42
// so the file-existence check must come FIRST. Only when the input is not
43
// an existing file do we fall back to the inline-source heuristic.
44
static bool stringIsScript(const QString& input)
64 ✔
45
{
46
  if(input.isEmpty())
64 ✔
47
    return false;
×
48

49
  if(QFileInfo fileInfo{input}; fileInfo.exists() && fileInfo.isFile())
124 ✔
50
    return false;
60 ✔
51

52
  if(input.length() > 4096)
4 ✔
53
    return true;
×
54

55
  for(QChar ch : input)
56 ✔
56
  {
57
    const char16_t c = ch.unicode();
55 ✔
58
    if(c == '\n' || c == '\r' || c == ';' || c == '{' || c == '}' || c == '('
55 ✔
59
       || c == ')')
55 ✔
60
      return true;
3 ✔
61
  }
62

63
  return false;
1 ✔
64
}
64 ✔
65

66
ApplicationPlugin::ApplicationPlugin(const score::GUIApplicationContext& ctx)
1,854 ✔
67
    : score::GUIApplicationPlugin{ctx}
1,854 ✔
68
{
1,854 ✔
69
#if __has_include(<QQuickWindow>) && !defined(__APPLE__)
70
  // Crisp text in every QML UI: distance-field rendering looks blurry at the small
71
  // font sizes our panels use, native glyph rendering matches the rest of score.
72
  // Not on macOS: https://qt-project.atlassian.net/browse/QTBUG-150490
73
  QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
1,854 ✔
74
#endif
75

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

107
  // For scripts of processes that run in the ui thread:
108
  m_scriptProcessUIEngine.globalObject().setProperty(
3,708 ✔
109
      "Util", m_scriptProcessUIEngine.newQObject(new JsUtils));
1,854 ✔
110
  m_scriptProcessUIEngine.globalObject().setProperty(
3,708 ✔
111
      "System", m_scriptProcessUIEngine.newQObject(new JsSystem));
1,854 ✔
112
  m_scriptProcessUIEngine.globalObject().setProperty(
3,708 ✔
113
      "Library", m_scriptProcessUIEngine.newQObject(new JsLibrary));
1,854 ✔
114
  m_scriptProcessUIEngine.globalObject().setProperty(
3,708 ✔
115
      "View", m_scriptProcessUIEngine.newQObject(new JsViewContext));
1,854 ✔
116

117
  // Command-line option parsing
118
  QCommandLineParser parser;
1,854 ✔
119

120
  QCommandLineOption script_opt(
1,854 ✔
121
      "script", QCoreApplication::translate("js", "script"), "Script", "");
1,854 ✔
122
  parser.addOption(script_opt);
1,854 ✔
123

124
  parser.parse(ctx.applicationSettings.arguments);
1,854 ✔
125
  for(const QString& script : parser.values(script_opt))
1,918 ✔
126
  {
127
    if(script.isEmpty())
64 ✔
128
      continue;
×
129

130
    if(stringIsScript(script))
64 ✔
131
    {
132
      this->m_start_scripts.push_back(StartScript{.source = script});
3 ✔
133
      continue;
134
    }
135

136
    QFile f{script};
61 ✔
137
    if(!f.open(QIODevice::ReadOnly))
61 ✔
138
    {
139
      qCritical() << "--script: cannot open" << script << ":" << f.errorString();
1 ✔
140
      this->m_start_script_failed = true;
1 ✔
141
      continue;
1 ✔
142
    }
143

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

155
    this->m_start_scripts.push_back(std::move(s));
60 ✔
156
  }
61 ✔
157
}
×
158

159
QJSValue ApplicationPlugin::importModule(QQmlEngine& engine, const QString& path)
4 ✔
160
{
161
  QJSValue mod = engine.importModule(path);
4 ✔
162
  if(mod.isError())
4 ✔
163
    return mod;
1 ✔
164

165
  if(auto init = mod.property("initialize"); init.isCallable())
5 ✔
166
    if(const auto res = init.call(); res.isError())
4 ✔
167
      return res;
1 ✔
168

169
  engine.globalObject().setProperty(QFileInfo{path}.baseName(), mod);
2 ✔
170
  return mod;
2 ✔
171
}
4 ✔
172

173
void ApplicationPlugin::on_newDocument(score::Document& doc)
625 ✔
174
{
175
  score::addDocumentPlugin<DocumentPlugin>(doc);
625 ✔
176
}
625 ✔
177

178
ApplicationPlugin::~ApplicationPlugin()
3,706 ✔
179
{
1,853 ✔
180
  m_processMessages = false;
1,853 ✔
181
  m_asioContext->context.stop();
1,853 ✔
182
  m_asioThread.join();
1,853 ✔
183
}
3,706 ✔
184

185
void ApplicationPlugin::on_createdDocument(score::Document& doc)
657 ✔
186
{
187
  // Local Tree
188
  LocalTree::DocumentPlugin* lt = doc.context().findPlugin<LocalTree::DocumentPlugin>();
657 ✔
189
  if(lt)
657 ✔
190
  {
191
    auto& root = lt->device().get_root_node();
657 ✔
192

193
    auto node = root.create_child("script");
657 ✔
194
    auto address = node->create_parameter(ossia::val_type::STRING);
657 ✔
195
    address->set_value(std::string{});
657 ✔
196
    address->set_access(ossia::access_mode::SET);
657 ✔
197
    address->add_callback([&](const ossia::value& v) {
687 ✔
198
      ossia::qt::run_async(
30 ✔
199
          this, [this, str = QString::fromStdString(ossia::convert<std::string>(v))] {
60 ✔
200
        auto res = m_consoleEngine.evaluate(str);
30 ✔
201
        if(res.isError())
30 ✔
202
        {
203
          qDebug() << res.toString();
20 ✔
204
        }
20 ✔
205
      });
30 ✔
206
    });
30 ✔
207
  }
657 ✔
208

209
  // Custom data
210
  if(auto customData = doc.context().findPlugin<DocumentPlugin>(); !customData)
657 ✔
211
    score::addDocumentPlugin<DocumentPlugin>(doc);
×
212

213
  if(m_start_script_failed)
657 ✔
214
  {
215
    qGuiApp->exit(2);
1 ✔
216
    return;
1 ✔
217
  }
218

219
}
657 ✔
220

221
void ApplicationPlugin::runStartScripts()
48 ✔
222
{
223
  for(const StartScript& s : m_start_scripts)
93 ✔
224
  {
225
    if(!s.dir.isEmpty())
49 ✔
226
      m_consoleEngine.addImportPath(s.dir);
47 ✔
227

228
    // Report a throwing --script: an unresolvable readFile returns an empty
229
    // string and eval("") is a no-op, so the process would otherwise exit
230
    // reporting success and a harness could not tell that from a pass.
231
    const auto res = s.module ? importModule(m_consoleEngine, s.file)
49 ✔
232
                              : m_consoleEngine.evaluate(s.source, s.name);
45 ✔
233
    if(res.isError())
49 ✔
234
    {
235
      qCritical().noquote()
8 ✔
236
          << "--script:" << (s.name.isEmpty() ? QStringLiteral("<inline>") : s.name)
4 ✔
237
          << "line" << res.property("lineNumber").toInt() << ":" << res.toString();
4 ✔
238
      qGuiApp->exit(3);
4 ✔
239
      return;
4 ✔
240
    }
241
  }
49 ✔
242
}
48 ✔
243
void ApplicationPlugin::initialize()
1,854 ✔
244
{
245
  // Documents named on the command line are loaded before afterStartup(), and
246
  // compiling one of their scripts is what makes the library's QML modules
247
  // needed: the import paths have to be in place by then.
248
  for(auto& p : this->context.settings<Library::Settings::Model>().getIncludePaths())
1,854 ✔
249
  {
250
    m_scriptProcessUIEngine.addImportPath(p);
×
251
    // The console engine runs --script, the console panel and every library
252
    // .mjs; without this they could not import what a JS process can, which
253
    // made the same `import` line work in a process and fail in a script.
254
    m_consoleEngine.addImportPath(p);
×
255
  }
256
}
1,854 ✔
257

258
void ApplicationPlugin::afterStartup()
62 ✔
259
{
260
  if(!m_start_scripts.empty())
62 ✔
261
  {
262
    QTimer::singleShot(
61 ✔
263
        (1 + context.applicationSettings.waitAfterLoad) * 1000, this,
61 ✔
264
        [this] { runStartScripts(); });
109 ✔
265
  }
61 ✔
266

267
#if __has_include(<QQuickWindow>)
268
  if(QFileInfo f{context.applicationSettings.ui}; f.isFile())
66 ✔
269
  {
270
    m_comp = new QQmlComponent{&m_consoleEngine, f.absoluteFilePath(), this};
4 ✔
271

272
    if(auto obj = m_comp->create())
4 ✔
273
    {
274
      if(auto item = qobject_cast<QQuickItem*>(obj))
1 ✔
275
      {
276
        m_window = new QQuickWindow{};
×
277
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
278
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
279
  // leaves the window with no title bar, close or minimise button.
280
  m_window->setFlags(
×
281
      m_window->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
×
282
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
×
283
      | Qt::WindowMaximizeButtonHint);
×
284
#if defined(__EMSCRIPTEN__)
285
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
286
  // turns into showFullScreen() for every top level: the requested size is
287
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
288
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
289
  m_window->setFlags(m_window->flags() | Qt::Dialog);
290
#endif
291
        m_window->setWidth(640);
×
292
        m_window->setHeight(480);
×
293
        item->setParentItem(m_window->contentItem());
×
294
#if defined(__EMSCRIPTEN__)
295
        m_window->showNormal();
296
#else
297
        m_window->show();
×
298
#endif
299
        return;
×
300
      }
301
    }
1 ✔
302
    else
303
    {
304
      qDebug() << m_comp->errorString();
3 ✔
305
      qGuiApp->exit(1);
3 ✔
306
    }
307
    delete m_comp;
4 ✔
308
  }
4 ✔
309
#endif
310
}
62 ✔
311
}
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