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

ossia / libossia / 36742070330

30 Sep 2026 04:08PM UTC coverage: 52.335% (+0.3%) from 52.019%
36742070330

push

github

jcelerier
math_expression: bound the loops of one evaluation

An expression such as `while(true) {}`, typed by a user or received as a
message, hung the execution thread. Every compiled loop now shares one
budget per evaluation (exprtk's own count is per loop and restarts on each
entry, so nested or successive loops would multiply it): a million
iterations, and a 100 ms backstop for loops whose bodies are themselves
expensive. When it runs out, every enclosing loop exits on its next test
rather than throwing through the audio thread; value() is then NaN,
result() empty, and interrupted() tells why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX

31944 of 61038 relevant lines covered (52.33%)

174558.16 hits per line

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

99.36
/tests/Dataflow/TokenRequestTest.cpp
1
#include "../Editor/TestUtils.hpp"
2

3
#include <ossia/detail/config.hpp>
4

5
#include <ossia/dataflow/token_request.hpp>
6

7
#include "include_catch.hpp"
8

9
#include <cmath>
10
#include <iterator>
11
#include <utility>
12
#include <vector>
13
TEST_CASE("test_loops_integer", "test_loops_integer")
1 ✔
14
{
15
  using namespace ossia;
16

17
  token_request r;
1 ✔
18
  r.prev_date = 0_tv;
1 ✔
19
  r.date = 100_tv;
1 ✔
20

21
  token_request_vec vec;
1 ✔
22
  r.loop(
1 ✔
23
      0_tv, 10_tv, [&](auto& req) { vec.push_back(req); }, [](const auto&) {});
20 ✔
24

25
  simple_token_request_vec expected{
26
      {.prev_date = 0_tv, .date = 10_tv, .offset = 0_tv},
27
      {.prev_date = 0_tv, .date = 10_tv, .offset = 10_tv},
28
      {.prev_date = 0_tv, .date = 10_tv, .offset = 20_tv},
29
      {.prev_date = 0_tv, .date = 10_tv, .offset = 30_tv},
30
      {.prev_date = 0_tv, .date = 10_tv, .offset = 40_tv},
31
      {.prev_date = 0_tv, .date = 10_tv, .offset = 50_tv},
32
      {.prev_date = 0_tv, .date = 10_tv, .offset = 60_tv},
33
      {.prev_date = 0_tv, .date = 10_tv, .offset = 70_tv},
34
      {.prev_date = 0_tv, .date = 10_tv, .offset = 80_tv},
35
      {.prev_date = 0_tv, .date = 10_tv, .offset = 90_tv}};
1 ✔
36

37
  REQUIRE(expected == vec);
1 ✔
38
}
1 ✔
39

40
TEST_CASE("test_loops_fract", "test_loops_fract")
1 ✔
41
{
42
  using namespace ossia;
43

44
  token_request r;
1 ✔
45
  r.prev_date = 0_tv;
1 ✔
46
  r.date = 97_tv;
1 ✔
47

48
  token_request_vec vec;
1 ✔
49
  r.loop(
1 ✔
50
      0_tv, 10_tv, [&](auto& req) { vec.push_back(req); }, [](const auto&) {});
19 ✔
51

52
  simple_token_request_vec expected{
53
      {.prev_date = 0_tv, .date = 10_tv, .offset = 0_tv},
54
      {.prev_date = 0_tv, .date = 10_tv, .offset = 10_tv},
55
      {.prev_date = 0_tv, .date = 10_tv, .offset = 20_tv},
56
      {.prev_date = 0_tv, .date = 10_tv, .offset = 30_tv},
57
      {.prev_date = 0_tv, .date = 10_tv, .offset = 40_tv},
58
      {.prev_date = 0_tv, .date = 10_tv, .offset = 50_tv},
59
      {.prev_date = 0_tv, .date = 10_tv, .offset = 60_tv},
60
      {.prev_date = 0_tv, .date = 10_tv, .offset = 70_tv},
61
      {.prev_date = 0_tv, .date = 10_tv, .offset = 80_tv},
62
      {.prev_date = 0_tv, .date = 7_tv, .offset = 90_tv}};
1 ✔
63

64
  REQUIRE(expected == vec);
1 ✔
65
}
1 ✔
66

67
TEST_CASE("test_loops_offset", "test_loops_offset")
1 ✔
68
{
69
  using namespace ossia;
70

71
  token_request r;
1 ✔
72
  r.prev_date = 2_tv;
1 ✔
73
  r.date = 12_tv;
1 ✔
74

75
  token_request_vec vec;
1 ✔
76
  r.loop(
1 ✔
77
      0_tv, 5_tv, [&](auto& req) { vec.push_back(req); }, [](const auto&) {});
5 ✔
78

79
  simple_token_request_vec expected{
80
      {.prev_date = 2_tv, .date = 5_tv, .offset = 0_tv},
81
      {.prev_date = 0_tv, .date = 5_tv, .offset = 3_tv},
82
      {.prev_date = 0_tv, .date = 2_tv, .offset = 8_tv},
83
  };
1 ✔
84

85
  REQUIRE(expected == vec);
1 ✔
86
}
1 ✔
87

88
TEST_CASE("test_loops_smaller", "test_loops_smaller")
1 ✔
89
{
90
  using namespace ossia;
91

92
  token_request r;
1 ✔
93
  r.prev_date = 0_tv;
1 ✔
94
  r.date = 15_tv;
1 ✔
95

96
  token_request_vec vec;
1 ✔
97
  r.loop(
1 ✔
98
      0_tv, 20_tv, [&](auto& req) { vec.push_back(req); }, [](const auto&) {});
1 ✔
99

100
  simple_token_request_vec expected{{.prev_date = 0_tv, .date = 15_tv, .offset = 0_tv}};
1 ✔
101

102
  REQUIRE(expected == vec);
1 ✔
103
}
1 ✔
104

105
TEST_CASE("test_loops_mid", "test_loops_mid")
1 ✔
106
{
107
  using namespace ossia;
108

109
  token_request r;
1 ✔
110
  r.prev_date = 15_tv;
1 ✔
111
  r.date = 30_tv;
1 ✔
112

113
  token_request_vec vec;
1 ✔
114
  r.loop(
1 ✔
115
      0_tv, 20_tv, [&](auto& req) { vec.push_back(req); }, [](const auto&) {});
3 ✔
116

117
  simple_token_request_vec expected{
118
      {.prev_date = 15_tv, .date = 20_tv, .offset = 0_tv},
119
      {.prev_date = 0_tv, .date = 10_tv, .offset = 5_tv}};
1 ✔
120

121
  REQUIRE(expected == vec);
1 ✔
122
}
1 ✔
123

124
TEST_CASE("test_musical_quantization", "test_musical_quantization")
1 ✔
125
{
126
  using namespace ossia;
127

128
  token_request r;
1 ✔
129
  r.prev_date = 0_tv;
1 ✔
130
  r.date = 2048000_tv;
1 ✔
131
  r.musical_end_last_bar = 0.005804985827664399;
1 ✔
132
  r.musical_end_position = 0.005804985827664399;
1 ✔
133

134
  auto res = r.get_quantification_date(8.0);
1 ✔
135
  REQUIRE(res == 0_tv);
1 ✔
136
}
1 ✔
137

138
namespace
139
{
140
using date_list = std::vector<std::pair<int64_t, int64_t>>;
141

142
//! A tick covering [prev; date[ in model time and [mstart; mend[ in quarters.
143
ossia::token_request musical_tick(
15 ✔
144
    int64_t prev, int64_t date, double mstart, double mend, uint16_t num = 4, uint16_t denom = 4)
145
{
146
  ossia::token_request t;
15 ✔
147
  t.prev_date = ossia::time_value{prev};
15 ✔
148
  t.date = ossia::time_value{date};
15 ✔
149
  t.musical_start_position = mstart;
15 ✔
150
  t.musical_end_position = mend;
15 ✔
151
  t.musical_start_last_bar = 0.;
15 ✔
152
  t.musical_start_last_signature = 0.;
15 ✔
153
  t.signature = ossia::time_signature{num, denom};
15 ✔
154
  return t;
15 ✔
155
}
156

157
date_list dates(const ossia::quantification_points& p)
9 ✔
158
{
159
  date_list out;
9 ✔
160
  for(auto& q : p)
46 ✔
161
    out.emplace_back(q.date.impl, q.index);
14 ✔
162
  return out;
9 ✔
163
}
×
164
}
165

166
TEST_CASE("quantification_dates_empty_tick", "quantification_dates")
1 ✔
167
{
168
  auto t = musical_tick(100, 100, 0., 1.);
1 ✔
169
  REQUIRE(t.get_quantification_dates(4).empty());
1 ✔
170
}
1 ✔
171

172
TEST_CASE("quantification_dates_no_rate", "quantification_dates")
1 ✔
173
{
174
  // A null or negative rate means "as soon as possible".
175
  auto t = musical_tick(100, 200, 0., 1.);
1 ✔
176
  REQUIRE(dates(t.get_quantification_dates(0)) == date_list{{100, 0}});
3 ✔
177
  REQUIRE(dates(t.get_quantification_dates(-1)) == date_list{{100, 0}});
3 ✔
178
}
1 ✔
179

180
TEST_CASE("quantification_dates_no_musical_info", "quantification_dates")
1 ✔
181
{
182
  // Without a musical position there is nothing to quantize against: the tick
183
  // itself is the point. Several nodes rely on this to step once per tick.
184
  auto t = musical_tick(100, 200, 0., 0.);
1 ✔
185
  REQUIRE(dates(t.get_quantification_dates(16)) == date_list{{100, 0}});
3 ✔
186
}
1 ✔
187

188
TEST_CASE("quantification_dates_several_per_tick", "quantification_dates")
1 ✔
189
{
190
  // The whole point of the multi-date version: one tick worth a quarter note
191
  // holds four sixteenths, and none of them may be dropped.
192
  auto t = musical_tick(0, 1000, 0., 1.);
1 ✔
193

194
  const auto expected = date_list{{0, 0}, {250, 1}, {500, 2}, {750, 3}};
2 ✔
195
  REQUIRE(dates(t.get_quantification_dates(16)) == expected);
1 ✔
196
}
1 ✔
197

198
TEST_CASE("quantification_dates_interior_point", "quantification_dates")
1 ✔
199
{
200
  auto t = musical_tick(0, 1000, 0.5, 1.5);
1 ✔
201
  const auto res = t.get_quantification_dates(4);
1 ✔
202

203
  REQUIRE(dates(res) == date_list{{500, 1}});
3 ✔
204
  // and it agrees with the single-date version
205
  auto single = t.get_quantification_date(4);
1 ✔
206
  REQUIRE(single.has_value());
1 ✔
207
  REQUIRE(*single == res[0].date);
1 ✔
208
}
1 ✔
209

210
TEST_CASE("quantification_dates_on_the_start_of_the_tick", "quantification_dates")
1 ✔
211
{
212
  auto t = musical_tick(400, 1000, 1., 1.5);
1 ✔
213
  const auto res = t.get_quantification_dates(4);
1 ✔
214

215
  REQUIRE(dates(res) == date_list{{400, 1}});
3 ✔
216

217
  auto single = t.get_quantification_date(4);
1 ✔
218
  REQUIRE(single.has_value());
1 ✔
219
  REQUIRE(*single == res[0].date);
1 ✔
220
}
1 ✔
221

222
TEST_CASE("quantification_dates_on_the_end_of_the_tick", "quantification_dates")
1 ✔
223
{
224
  // A point falling exactly on the end of a tick belongs to the next one,
225
  // otherwise it would fire twice.
226
  auto t = musical_tick(0, 1000, 0.5, 1.);
1 ✔
227
  REQUIRE(t.get_quantification_dates(4).empty());
1 ✔
228
  REQUIRE(!t.get_quantification_date(4).has_value());
1 ✔
229
}
1 ✔
230

231
TEST_CASE("quantification_dates_bars", "quantification_dates")
1 ✔
232
{
233
  // rate <= 1 counts bars: 1 is every bar, 0.5 every two bars.
234
  auto t = musical_tick(0, 1000, 0., 8.);
1 ✔
235

236
  REQUIRE(dates(t.get_quantification_dates(1)) == date_list{{0, 0}, {500, 1}});
3 ✔
237
  REQUIRE(dates(t.get_quantification_dates(0.5)) == date_list{{0, 0}});
3 ✔
238
}
1 ✔
239

240
TEST_CASE("quantification_dates_odd_signature", "quantification_dates")
1 ✔
241
{
242
  // 7/8: a bar is 3.5 quarters.
243
  auto t = musical_tick(0, 1000, 0., 7., 7, 8);
1 ✔
244
  REQUIRE(dates(t.get_quantification_dates(1)) == date_list{{0, 0}, {500, 1}});
3 ✔
245
}
1 ✔
246

247
TEST_CASE("quantification_dates_are_ordered_and_inside_the_tick", "quantification_dates")
1 ✔
248
{
249
  for(double rate : {1., 2., 4., 8., 16., 32.})
7 ✔
250
  {
251
    auto t = musical_tick(0, 4096, 0.3, 5.7);
6 ✔
252
    const auto res = t.get_quantification_dates(rate);
6 ✔
253

254
    INFO("rate " << rate);
6 ✔
255
    int64_t previous = -1;
6 ✔
256
    for(const auto& q : res)
178 ✔
257
    {
258
      CHECK(q.date.impl >= t.prev_date.impl);
83 ✔
259
      CHECK(q.date.impl < t.date.impl);
83 ✔
260
      CHECK(q.date.impl >= previous);
83 ✔
261
      previous = q.date.impl;
83 ✔
262
    }
263
  }
6 ✔
264
}
1 ✔
265

266
TEST_CASE("musical_to_frame_grid_point_is_not_one_sample_early", "musical_to_frame")
1 ✔
267
{
268
  // 1 kHz, 120 BPM: an eighth note is 250 frames. The positions come out of
269
  // divisions that round them a few ulps below the grid point.
270
  CHECK(ossia::musical_to_frame(0.5, 0.384, 0.512, 64) == 58);
1 ✔
271
  CHECK(ossia::musical_to_frame(0.5, 0., 0.666, 333) == 250);
1 ✔
272
  CHECK(ossia::musical_to_frame(1.5, 1.4000000000000001, 1.6, 100) == 50);
1 ✔
273

274
  // Engine-style ticks: every grid point that falls exactly on a sample lands on it.
275
  for(double rate : {44100., 48000., 96000.})
4 ✔
276
    for(double tempo : {97.3, 120., 133.7})
12 ✔
277
      for(int buffer : {1, 7, 64, 333, 512})
54 ✔
278
      {
279
        const double frames_per_quarter = rate * 60. / tempo;
45 ✔
280
        for(int64_t t = 0; t < 500; t++)
22,545 ✔
281
        {
282
          const double a = t * buffer / frames_per_quarter;
22,500 ✔
283
          const double b = (t + 1) * buffer / frames_per_quarter;
22,500 ✔
284
          for(auto k = int64_t(std::ceil(a / 0.5)); k * 0.5 < b; k++)
22,817 ✔
285
          {
286
            const double exact = k * 0.5 * frames_per_quarter - double(t * buffer);
317 ✔
287
            const double nearest = std::round(exact);
317 ✔
288
            if(std::abs(exact - nearest) > 1e-6 || nearest < 0 || nearest >= buffer)
317 ✔
289
              continue;
179 ✔
290
            INFO(rate << " " << tempo << " " << buffer << " " << t << " " << k);
138 ✔
291
            CHECK(ossia::musical_to_frame(k * 0.5, a, b, buffer) == int64_t(nearest));
138 ✔
292
          }
138 ✔
293
        }
294
      }
295
}
1 ✔
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