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

ossia / score / 36213733900

26 Sep 2026 03:05AM UTC coverage: 42.353% (+0.1%) from 42.23%
36213733900

Pull #2310

github

web-flow
Merge 9f9d60cd9 into c0c64ec39
Pull Request #2310: bitfocus: host companion modules the way companion does

41 of 2024 new or added lines in 11 files covered. (2.03%)

210 existing lines in 15 files now uncovered.

126773 of 299323 relevant lines covered (42.35%)

118427.51 hits per line

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

0.0
/src/plugins/score-plugin-protocols/Tests/BitfocusModuleDriver.cpp
1
// Drives one companion module through score's Bitfocus host: the settings
2
// widget computes the configuration a user would save, then a device is built
3
// from it as BitfocusDevice::reconnect does, and every action of the tree is
4
// triggered. Used by tests/tools/bitfocus/sweep.py to compare score with a
5
// reference host reproducing companion.
6
#include <Protocols/Bitfocus/BitfocusContext.hpp>
7
#include <Protocols/Bitfocus/BitfocusProtocol.hpp>
8
#include <Protocols/Bitfocus/BitfocusProtocolFactory.hpp>
9
#include <Protocols/Bitfocus/BitfocusProtocolSettingsWidget.hpp>
10
#include <Protocols/Bitfocus/BitfocusSpecificSettings.hpp>
11

12
#include <ossia/detail/algorithms.hpp>
13
#include <ossia/network/base/node_attributes.hpp>
14
#include <ossia/network/common/complex_type.hpp>
15
#include <ossia/network/value/format_value.hpp>
16

17
#include <ossia-qt/js_utilities.hpp>
18

19
#include <score_test/App.hpp>
20

21
#include <QApplication>
22
#include <QDateTime>
23
#include <QDir>
24
#include <QElapsedTimer>
25
#include <QFile>
26
#include <QFileInfo>
27
#include <QJsonArray>
28
#include <QJsonDocument>
29
#include <QJsonObject>
30
#include <QTimer>
31
#include <QUdpSocket>
32

33
#include <chrono>
34
#include <cstdio>
35

36
namespace bitfocus
37
{
NEW
38
QString nodeExecutable(const QString& nodeVersion)
×
39
{
NEW
40
  const QString root = qEnvironmentVariable("BITFOCUS_NODE_RUNTIME");
×
NEW
41
  for(const auto& dir : QDir{root}.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
×
NEW
42
    if("node" + dir.split('.').front() == nodeVersion)
×
NEW
43
      return root + "/" + dir + "/bin/node";
×
NEW
44
  return "node";
×
NEW
45
}
×
46
}
47

48
namespace
49
{
50
FILE* g_out{};
51

NEW
52
void emit(QJsonObject o)
×
53
{
NEW
54
  o["t"] = std::chrono::duration<double>(
×
NEW
55
                std::chrono::system_clock::now().time_since_epoch())
×
NEW
56
                .count();
×
NEW
57
  auto str = QJsonDocument{o}.toJson(QJsonDocument::Compact);
×
NEW
58
  std::fprintf(g_out, "%s\n", str.constData());
×
NEW
59
  std::fflush(g_out);
×
NEW
60
}
×
61

NEW
62
bool waitFor(const std::function<bool()>& pred, int ms)
×
63
{
NEW
64
  QElapsedTimer t;
×
NEW
65
  t.start();
×
NEW
66
  while(!pred() && t.elapsed() < ms)
×
NEW
67
    QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
×
NEW
68
  return pred();
×
69
}
70

NEW
71
void sleepFor(int ms)
×
72
{
NEW
73
  waitFor([] { return false; }, ms);
×
NEW
74
}
×
75

NEW
76
QJsonValue toJson(const ossia::value& v)
×
77
{
NEW
78
  return QJsonValue::fromVariant(v.apply(ossia::qt::ossia_to_qvariant{}));
×
NEW
79
}
×
80

NEW
81
Protocols::BitfocusSpecificSettings settingsFromManifest(const QString& path)
×
82
{
NEW
83
  Protocols::BitfocusSpecificSettings set;
×
NEW
84
  QFile f{path + "/companion/manifest.json"};
×
NEW
85
  f.open(QIODevice::ReadOnly);
×
NEW
86
  auto doc = QJsonDocument::fromJson(f.readAll()).object();
×
NEW
87
  auto rt = doc["runtime"].toObject();
×
NEW
88
  set.path = path;
×
NEW
89
  set.id = doc["id"].toString();
×
NEW
90
  set.name = doc["shortname"].toString();
×
NEW
91
  set.brand = doc["manufacturer"].toString();
×
NEW
92
  set.nodeVersion = rt["type"].toString() == "node18" ? "node18" : "node22";
×
NEW
93
  set.apiVersion = rt["apiVersion"].toString();
×
NEW
94
  set.entrypoint = rt["entrypoint"].toString();
×
NEW
95
  set.entrypoint.remove("../");
×
NEW
96
  if(auto prods = doc["products"].toArray(); !prods.isEmpty())
×
NEW
97
    set.product = prods.first().toString();
×
NEW
98
  return set;
×
NEW
99
}
×
100

NEW
101
QJsonObject describeNode(ossia::net::node_base& n)
×
102
{
NEW
103
  QJsonObject o;
×
NEW
104
  o["name"] = QString::fromStdString(n.get_name());
×
NEW
105
  if(auto p = n.get_parameter())
×
106
  {
NEW
107
    o["type"] = int(p->get_value_type());
×
NEW
108
    o["value"] = toJson(p->value());
×
NEW
109
  }
×
NEW
110
  QJsonArray cld;
×
NEW
111
  for(auto& c : n.children())
×
NEW
112
    cld.append(describeNode(*c));
×
NEW
113
  if(!cld.isEmpty())
×
NEW
114
    o["children"] = cld;
×
NEW
115
  return o;
×
NEW
116
}
×
117
}
118

NEW
119
int run(int argc, char** argv)
×
120
{
121

NEW
122
  QString modulePath, outPath, configPath, scenarioPath;
×
NEW
123
  int settle = 2500, gap = 120;
×
NEW
124
  for(int i = 1; i + 1 < argc; i += 2)
×
125
  {
NEW
126
    QString k = argv[i], v = argv[i + 1];
×
NEW
127
    if(k == "--module")
×
NEW
128
      modulePath = v;
×
NEW
129
    else if(k == "--out")
×
NEW
130
      outPath = v;
×
NEW
131
    else if(k == "--config")
×
NEW
132
      configPath = v;
×
NEW
133
    else if(k == "--settle")
×
NEW
134
      settle = v.toInt();
×
NEW
135
    else if(k == "--gap")
×
NEW
136
      gap = v.toInt();
×
NEW
137
    else if(k == "--scenario")
×
NEW
138
      scenarioPath = v;
×
NEW
139
  }
×
NEW
140
  g_out = std::fopen(outPath.toUtf8().constData(), "w");
×
NEW
141
  if(!g_out)
×
NEW
142
    return 1;
×
143

144
  // Values the user types in the settings dialog: the host fields.
NEW
145
  QJsonObject overrides;
×
146
  {
NEW
147
    QFile f{configPath};
×
NEW
148
    if(f.open(QIODevice::ReadOnly))
×
NEW
149
      overrides = QJsonDocument::fromJson(f.readAll()).object()["overrides"].toObject();
×
NEW
150
  }
×
151

152
  // Explicit config values, actions with options, and packets from the device
NEW
153
  QJsonObject scenario;
×
154
  {
NEW
155
    QFile f{scenarioPath};
×
NEW
156
    if(f.open(QIODevice::ReadOnly))
×
NEW
157
      scenario = QJsonDocument::fromJson(f.readAll()).object();
×
NEW
158
    auto conf = scenario["config"].toObject();
×
NEW
159
    for(auto it = conf.begin(); it != conf.end(); ++it)
×
NEW
160
      overrides[it.key()] = it.value();
×
NEW
161
  }
×
162

NEW
163
  auto base = settingsFromManifest(modulePath);
×
NEW
164
  const QString deviceName = "dev";
×
165

166
  // 1. Settings dialog
NEW
167
  Protocols::BitfocusSpecificSettings saved;
×
168
  {
NEW
169
    auto stgs = base;
×
170

NEW
171
    Device::DeviceSettings ds;
×
NEW
172
    ds.name = deviceName;
×
NEW
173
    ds.protocol = Protocols::BitfocusProtocolFactory::static_concreteKey();
×
NEW
174
    ds.deviceSpecificSettings = QVariant::fromValue(stgs);
×
175

NEW
176
    Protocols::BitfocusProtocolSettingsWidget w;
×
NEW
177
    w.setSettings(ds);
×
178
    auto handler
NEW
179
        = w.getSettings().deviceSpecificSettings.value<Protocols::BitfocusSpecificSettings>().handler;
×
180
    // As a user would: the dialog is read once the module has initialised
NEW
181
    bool inited = false;
×
NEW
182
    if(handler)
×
NEW
183
      handler->afterRegistration([&inited] { inited = true; });
×
NEW
184
    bool ok = handler && waitFor([&] { return inited; }, 15000);
×
NEW
185
    QCoreApplication::processEvents();
×
NEW
186
    emit({{"ev", "widget-fields"}, {"ok", ok}});
×
187

NEW
188
    saved = w.getSettings().deviceSpecificSettings.value<Protocols::BitfocusSpecificSettings>();
×
189
    // What the user types in the dialog
NEW
190
    for(auto it = overrides.begin(); it != overrides.end(); ++it)
×
191
    {
NEW
192
      auto v = ossia::qt::qt_to_ossia{}(it.value().toVariant());
×
NEW
193
      auto cur = ossia::find_if(
×
NEW
194
          saved.configuration, [&](auto& kv) { return kv.first == it.key(); });
×
NEW
195
      if(cur != saved.configuration.end())
×
NEW
196
        cur->second = v;
×
197
      else
NEW
198
        saved.configuration.emplace_back(it.key(), v);
×
NEW
199
    }
×
NEW
200
    QJsonObject conf;
×
NEW
201
    for(auto& [k, v] : saved.configuration)
×
NEW
202
      conf[k] = toJson(v);
×
NEW
203
    emit(
×
NEW
204
        {{"ev", "widget-config"},
×
NEW
205
         {"config", conf},
×
NEW
206
         {"upgradeIndex", saved.upgradeIndex ? QJsonValue(*saved.upgradeIndex) : QJsonValue()}});
×
NEW
207
    saved.handler.reset();
×
NEW
208
  }
×
NEW
209
  sleepFor(300);
×
210

211
  // 2. The device, as BitfocusDevice::reconnect builds it from saved settings
NEW
212
  auto ctx = std::make_shared<ossia::net::network_context>();
×
NEW
213
  QTimer poll;
×
NEW
214
  QObject::connect(&poll, &QTimer::timeout, [&] { ctx->context.poll(); });
×
NEW
215
  poll.start(5);
×
216

NEW
217
  std::shared_ptr<bitfocus::module_handler> handler;
×
NEW
218
  std::shared_ptr<ossia::net::generic_device> dev;
×
NEW
219
  bool registered = false;
×
220
  {
NEW
221
    auto stgs = saved;
×
NEW
222
    stgs.deduplicateConfiguration();
×
NEW
223
    handler = stgs.makeHandler(deviceName);
×
224

225

NEW
226
    auto proto = std::make_unique<ossia::net::bitfocus_protocol>(handler, ctx);
×
NEW
227
    auto pproto = proto.get();
×
NEW
228
    dev = std::make_shared<ossia::net::generic_device>(
×
NEW
229
        std::move(proto), deviceName.toStdString());
×
NEW
230
    handler->afterRegistration([&registered, pproto] {
×
NEW
231
      pproto->init_device();
×
NEW
232
      registered = true;
×
NEW
233
    });
×
NEW
234
  }
×
235

NEW
236
  int varSignals = 0, fbSignals = 0;
×
NEW
237
  QObject::connect(
×
NEW
238
      handler.get(), &bitfocus::module_handler::variableChanged, [&] { varSignals++; });
×
NEW
239
  QObject::connect(
×
NEW
240
      handler.get(), &bitfocus::module_handler::feedbackValueChanged,
×
NEW
241
      [&] { fbSignals++; });
×
242

NEW
243
  waitFor([&] { return registered; }, 20000);
×
NEW
244
  emit({{"ev", "registered"}, {"ok", registered}});
×
NEW
245
  sleepFor(settle);
×
246

NEW
247
  auto& root = dev->get_root_node();
×
NEW
248
  emit({{"ev", "tree"}, {"root", describeNode(root)}});
×
249

NEW
250
  if(auto actions = root.find_child(std::string_view("action"));
×
NEW
251
     actions && scenario.contains("actions"))
×
252
  {
NEW
253
    emit({{"ev", "actions-begin"}});
×
NEW
254
    for(auto a : scenario["actions"].toArray())
×
255
    {
NEW
256
      const auto obj = a.toObject();
×
NEW
257
      const auto id = obj["id"].toString();
×
NEW
258
      auto node = actions->find_child(id.toStdString());
×
NEW
259
      if(!node || !node->get_parameter())
×
NEW
260
        continue;
×
261
      // A fresh companion action instance starts from the defaults
NEW
262
      if(auto def = handler->model().actions.find(id); def != handler->model().actions.end())
×
NEW
263
        for(auto& opt : def->second.options)
×
NEW
264
          if(auto cld = node->find_child(opt.id.toStdString()))
×
NEW
265
            if(auto p = cld->get_parameter())
×
NEW
266
              p->set_value(ossia::net::bitfocus_protocol::optionDefault(opt));
×
NEW
267
      const auto opts = obj["options"].toObject();
×
NEW
268
      for(auto it = opts.begin(); it != opts.end(); ++it)
×
NEW
269
        if(auto cld = node->find_child(it.key().toStdString()))
×
NEW
270
          if(auto p = cld->get_parameter())
×
NEW
271
            p->set_value(ossia::qt::qt_to_ossia{}(it.value().toVariant()));
×
NEW
272
      emit({{"ev", "action"}, {"id", id}});
×
NEW
273
      node->get_parameter()->push_value(ossia::impulse{});
×
NEW
274
      sleepFor(gap);
×
NEW
275
    }
×
NEW
276
    QUdpSocket inj;
×
NEW
277
    for(auto i : scenario["inject"].toArray())
×
278
    {
NEW
279
      const auto obj = i.toObject();
×
NEW
280
      inj.writeDatagram(
×
NEW
281
          QByteArray::fromHex(obj["hex"].toString().toLatin1()), QHostAddress::LocalHost,
×
NEW
282
          obj["port"].toInt());
×
NEW
283
      sleepFor(obj["wait"].toInt(300));
×
NEW
284
    }
×
NEW
285
    emit({{"ev", "actions-end"}});
×
NEW
286
    sleepFor(5000);
×
NEW
287
  }
×
NEW
288
  else if(actions)
×
289
  {
NEW
290
    emit({{"ev", "actions-begin"}});
×
NEW
291
    std::vector<ossia::net::node_base*> nodes;
×
NEW
292
    for(auto& c : actions->children())
×
NEW
293
      nodes.push_back(c.get());
×
NEW
294
    for(auto* node : nodes)
×
295
    {
NEW
296
      auto p = node->get_parameter();
×
NEW
297
      if(!p)
×
NEW
298
        continue;
×
NEW
299
      emit({{"ev", "action"}, {"id", QString::fromStdString(node->get_name())}});
×
NEW
300
      p->push_value(ossia::impulse{});
×
NEW
301
      sleepFor(gap);
×
302
    }
NEW
303
    emit({{"ev", "actions-end"}});
×
304
    // As long as the reference waits for the replies
NEW
305
    sleepFor(5000);
×
NEW
306
  }
×
307

NEW
308
  QJsonObject vars;
×
NEW
309
  if(auto variables = root.find_child(std::string_view("variable")))
×
NEW
310
    for(auto& c : variables->children())
×
NEW
311
      if(auto p = c->get_parameter())
×
NEW
312
        vars[QString::fromStdString(c->get_name())] = toJson(p->value());
×
313

NEW
314
  const auto& m = handler->model();
×
NEW
315
  emit(
×
NEW
316
      {{"ev", "summary"},
×
NEW
317
       {"actions", int(m.actions.size())},
×
NEW
318
       {"feedbacks", int(m.feedbacks.size())},
×
NEW
319
       {"variables", int(m.variables.size())},
×
NEW
320
       {"variableValues", vars},
×
NEW
321
       {"varSignals", varSignals},
×
NEW
322
       {"fbSignals", fbSignals},
×
NEW
323
       {"tree", describeNode(root)}});
×
324

NEW
325
  dev.reset();
×
NEW
326
  handler.reset();
×
NEW
327
  std::fclose(g_out);
×
NEW
328
  return 0;
×
NEW
329
}
×
330

NEW
331
int main(int argc, char** argv)
×
332
{
333
  // run_in_app changes the working directory: paths are made absolute first
NEW
334
  std::vector<QByteArray> storage;
×
NEW
335
  std::vector<char*> args{argv[0]};
×
NEW
336
  for(int i = 1; i < argc; i++)
×
337
  {
NEW
338
    const bool isPath = i > 1 && QByteArray(argv[i - 1]) != "--gap"
×
NEW
339
                        && QByteArray(argv[i - 1]) != "--settle"
×
NEW
340
                        && QByteArray(argv[i - 1]).startsWith("--");
×
NEW
341
    storage.push_back(
×
NEW
342
        isPath ? QFileInfo(QString::fromLocal8Bit(argv[i])).absoluteFilePath().toLocal8Bit()
×
NEW
343
               : QByteArray(argv[i]));
×
NEW
344
  }
×
NEW
345
  for(auto& a : storage)
×
NEW
346
    args.push_back(a.data());
×
NEW
347
  argc = int(args.size());
×
NEW
348
  argv = args.data();
×
349

NEW
350
  int ret = 1;
×
NEW
351
  score::test::run_in_app([&](const score::GUIApplicationContext&) { ret = run(argc, argv); });
×
NEW
352
  return ret;
×
NEW
353
}
×
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