ifw-daq  3.0.0-pre2
IFW Data Acquisition modules
testOcmDaqService.cpp
Go to the documentation of this file.
1 /**
2  * @file
3  * @ingroup daq_ocm_server_test
4  * @copyright 2022 ESO - European Southern Observatory
5  *
6  * @brief Unit tests for OcmDaqService
7  */
8 #include <ocmDaqService.hpp>
9 
10 #include <fmt/format.h>
11 #include <gtest/gtest.h>
12 
13 #include <daq/error.hpp>
14 #include <malMock.hpp>
15 #include <metadaqifMock.hpp>
16 #include <recifMock.hpp>
17 
18 #include "daqifFake.hpp"
19 #include "mock/daqifMock.hpp"
20 #include "mock/managerMock.hpp"
21 
22 using namespace ::testing;
23 using namespace std::literals;
24 
25 /**
26  * Fixture that sets up a OcmDaqService instance with following mockups:
27  * - mal::Mal
28  * - daq::Manager
29  *
30  * And prepared request string containing metadata sources
31  *
32  * @ingroup daq_ocm_server_test
33  */
34 struct TestOcmDaqService : Test {
36  : m_io_ctx()
37  , m_mal_mock()
38  , m_mgr_mock()
39  , m_event_log(std::make_shared<daq::ObservableEventLog>())
40  , m_daq_impl(std::make_shared<OcmDaqService>(
41  m_io_ctx, m_mal_mock, m_mgr_mock, "ocm", "/tmp", m_event_log)) {
42  }
43  void SetUp() override {
44  m_metadata_sources = "meta@zpb.rr://uri";
45  m_prim_sources = "prim@zpb.rr://uri";
46  m_daq_properties = R"(
47  {
48  "keywords": [
49  {
50  "type": "esoKeyword",
51  "name": "FOO BAR",
52  "value": true
53  },
54  {
55  "type": "valueKeyword",
56  "name": "FOOBAR",
57  "value": true
58  }
59  ]
60  }
61  )";
62  m_daqv2_spec = R"(
63  {
64  "id": "id",
65  "sources": [
66  {
67  "type": "metadataSource",
68  "sourceName": "name",
69  "rrUri": "uri"
70  }
71  ]
72  }
73  )";
74  }
75  void TearDown() override {
76  }
77 
78  boost::asio::io_context m_io_ctx; // NOLINT
79  MalMock m_mal_mock; // NOLINT
81  std::shared_ptr<daq::ObservableEventLog> m_event_log;
82  std::shared_ptr<OcmDaqService> m_daq_impl; // NOLINT
83  std::string m_prim_sources; // NOLINT
84  std::string m_metadata_sources; // NOLINT
85  std::string m_daq_properties; // NOLINT
86  std::string m_daqv2_spec; // NOLINT
87 };
88 
89 /**
90  * Fixture for testing when OcmDaqService is abandoned
91  * (e.g. when service is de-registered).
92  *
93  * @ingroup daq_ocm_server_test
94  */
96 
97 TEST(TestParseSingleSource, Successful) {
98  ParsedSource expected("name", "zpb://rr.uri");
99 
100  ParsedSource parsed = ParseSourceUri(fmt::format("{}@{}", expected.name, expected.rr_uri));
101  EXPECT_EQ(expected, parsed);
102 }
103 
104 TEST(TestParseSingleSource, LeadingOrTrailingSpacesAreAllowed) {
105  ParsedSource expected("name", "zpb://rr.uri");
106  EXPECT_EQ(expected, ParseSourceUri(fmt::format("{}@{} ", expected.name, expected.rr_uri)));
107  EXPECT_EQ(expected, ParseSourceUri(fmt::format(" {}@{}", expected.name, expected.rr_uri)));
108  EXPECT_EQ(expected, ParseSourceUri(fmt::format(" {}@{} ", expected.name, expected.rr_uri)));
109 }
110 
111 TEST(TestParseSingleSource, Incomplete) {
112  ParsedSource expected("name", "zpb://rr.uri");
113  EXPECT_THROW(ParseSourceUri(fmt::format("{}@,{} ", expected.name, expected.rr_uri)),
114  std::invalid_argument);
115  EXPECT_THROW(ParseSourceUri(fmt::format("{}@,{} ", expected.name, expected.rr_uri)),
116  std::invalid_argument);
117  EXPECT_THROW(ParseSourceUri(fmt::format("@,")), std::invalid_argument);
118  EXPECT_THROW(ParseSourceUri(fmt::format("{},{}", expected.name, expected.rr_uri)),
119  std::invalid_argument);
120  EXPECT_THROW(ParseSourceUri(fmt::format("{}@,{}", expected.name, expected.rr_uri)),
121  std::invalid_argument);
122 }
123 
124 TEST(TestParseMultipleSources, Successful) {
125  ParsedSource expected1("name", "zpb://rr.uri");
126  ParsedSource expected2("name", "zpb://rr.uri");
127  std::vector<ParsedSource> result{expected1, expected2};
128 
129  EXPECT_EQ(
130  result,
131  ParseSourceUris(fmt::format(
132  "{}@{} {}@{}", expected1.name, expected1.rr_uri, expected2.name, expected2.rr_uri)));
133  EXPECT_EQ(
134  result,
135  ParseSourceUris(fmt::format(
136  "{}@{} {}@{}", expected1.name, expected1.rr_uri, expected2.name, expected2.rr_uri)));
137 
138  EXPECT_EQ(
139  result,
140  ParseSourceUris(fmt::format(
141  " {}@{} {}@{} ", expected1.name, expected1.rr_uri, expected2.name, expected2.rr_uri)));
142 }
143 
144 TEST(TestParseSingleSource, Empty) {
145  ParsedSource expected("name", "zpb://rr.uri");
146  EXPECT_THROW(ParseSourceUri(fmt::format("{}@,{} ", "", expected.rr_uri)),
147  std::invalid_argument);
148  EXPECT_THROW(ParseSourceUri(fmt::format("{}@,{} ", expected.name, "")), std::invalid_argument);
149  EXPECT_THROW(ParseSourceUri(fmt::format("{}@,{} ", expected.name, expected.rr_uri)),
150  std::invalid_argument);
151 }
152 
153 TEST(TestParseDaqContext, Successful) {
154  auto max_err_ms = 0.001;
155  {
156  auto properties_str = R"(
157  {
158  "keywords": [
159  {
160  "type": "esoKeyword",
161  "name": "FOO BAR",
162  "value": true
163  },
164  {
165  "type": "valueKeyword",
166  "name": "FOOBAR",
167  "value": true
168  }
169  ],
170  "awaitInterval": 0.1
171  }
172  )";
173  auto properties = ParseStartDaqContext(properties_str);
174  EXPECT_THAT(static_cast<double>(properties.await_interval.count()),
175  DoubleNear(100, max_err_ms));
176  }
177  {
178  auto properties_str = R"(
179  {
180  "awaitInterval": 1
181  }
182  )";
183  auto properties = ParseStartDaqContext(properties_str);
184  EXPECT_THAT(static_cast<double>(properties.await_interval.count()),
185  DoubleNear(1000, max_err_ms));
186  }
187 }
188 
189 TEST(TestParseDaqContext, Failures) {
190  EXPECT_THROW(ParseStartDaqContext(R"({"awaitInterval": "0.1"})"), std::invalid_argument);
191  EXPECT_THROW(ParseStartDaqContext(R"({"awaitInterval": -0.1})"), std::invalid_argument);
192 }
193 
194 TEST_F(TestOcmDaqService, StartDaqShouldTestIfIdAlreadyExistsAndReturnFailureIfItDoes) {
195  // Setup
196  // Setup so that ID already exist
197  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).WillOnce(Return(true));
198  // MakeDaqId is possibly called since a file_id is required.
199  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).Times(Between(0, 1)).WillOnce(Return("file-id"));
200 
201  // Run
202  auto fut = m_daq_impl->StartDaq("id", "prefix", m_prim_sources, m_metadata_sources, "");
203  m_io_ctx.poll();
204  ASSERT_TRUE(fut.is_ready()) << "future should be ready by now since daq with id already exist";
205  EXPECT_THROW(fut.get(), daqif::DaqException);
206 }
207 
208 TEST_F(TestOcmDaqService, StartDaqShouldCreateAndAssignIdIfNotProvided) {
209  // Setup
210  auto* fake_reply(new daqif::DaqReplyFake);
211 
212  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).WillOnce(Return("id"));
213  EXPECT_CALL(m_mgr_mock, StartDaqAsync(Field(&daq::DaqContext::id, std::string("id"))))
214  .WillOnce(Return(ByMove(boost::make_ready_future<daq::State>(daq::State::Acquiring))));
215  // Have to construct and return void* and still keep a reference to it..
216  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
217  .WillOnce(Return(fake_reply))
218  .RetiresOnSaturation();
219 
220  // Run
221  auto fut = m_daq_impl->StartDaq("", "prefix", m_prim_sources, m_metadata_sources, "");
222  // Requred to trigger any .then continuations, even though future is ready
223  m_io_ctx.poll();
224 
225  ASSERT_TRUE(fut.is_ready()) << "future should be ready immediately since we faked a "
226  "synchronous start with make_ready_future";
227  EXPECT_TRUE(fut.has_value());
228  auto reply = fut.get();
229  ASSERT_TRUE(reply);
230  EXPECT_EQ(reply->getId(), "id");
231 }
232 
233 TEST_F(TestOcmDaqService, StartDaqV2ShouldTestIfIdAlreadyExistsAndReturnFailureIfItDoes) {
234  // Setup
235  // Setup so that ID already exist
236  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).WillOnce(Return(true));
237  // MakeDaqId is possibly called since a file_id is required.
238  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).Times(Between(0, 1)).WillOnce(Return("file-id"));
239 
240  // Run
241  auto fut = m_daq_impl->StartDaqV2(m_daqv2_spec);
242  m_io_ctx.poll();
243  ASSERT_TRUE(fut.is_ready()) << "future should be ready by now since daq with id already exist";
244  EXPECT_THROW(fut.get(), daqif::DaqException);
245 }
246 
247 TEST_F(TestOcmDaqService, StartDaqFailsIfJsonIsInvalid) {
248  // Setup
249 
250  // Run
251  auto fut = m_daq_impl->StartDaq("id", "prefix", "", m_metadata_sources, "INVALID JSON");
252  m_io_ctx.poll();
253  ASSERT_TRUE(fut.is_ready()) << "future should be ready by now since daq with id already exist";
254  EXPECT_THROW(fut.get(), daqif::DaqException);
255 }
256 
257 TEST_F(TestOcmDaqService, StartDaqFailsIfJsonSchemaIsInvalid) {
258  // Setup
259 
260  // Run
261  // note: Expects JSON object
262  auto fut = m_daq_impl->StartDaq("id", "prefix", "", m_metadata_sources, "[]");
263  m_io_ctx.poll();
264  ASSERT_TRUE(fut.is_ready());
265  EXPECT_THROW(fut.get(), daqif::DaqException);
266 }
267 
268 TEST_F(TestOcmDaqService, StartDaqFailsIfPrefixContainsParentPaths) {
269  // Setup
270 
271  // Run
272  // note: Expects JSON object
273  auto fut = m_daq_impl->StartDaq("id", "/path/prefix", "", m_metadata_sources, m_daq_properties);
274  m_io_ctx.poll();
275  ASSERT_TRUE(fut.is_ready());
276  EXPECT_THROW(fut.get(), daqif::DaqException);
277 }
278 
279 TEST_F(TestOcmDaqService, StartDaqShouldAddAndStartDaqControllerIfArgumentsAreOk) {
280  // Setup
281  auto* fake_reply(new daqif::DaqReplyFake);
282 
283  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).Times(Between(0, 1)).WillOnce(Return("file-id"));
284  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).WillOnce(Return(false));
285  EXPECT_CALL(m_mgr_mock, StartDaqAsync(Field(&daq::DaqContext::id, "id")))
286  .WillOnce(Return(ByMove(boost::make_ready_future<daq::State>(daq::State::Acquiring))));
287  // Have to construct and return void* and still keep a reference to it..
288  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
289  .WillOnce(Return(fake_reply))
290  .RetiresOnSaturation();
291 
292  // Run
293  auto fut = m_daq_impl->StartDaq("id", "prefix", "", m_metadata_sources, m_daq_properties);
294 
295  // Requred to trigger any .then continuations, even though future is ready
296  m_io_ctx.poll();
297 
298  ASSERT_TRUE(fut.is_ready()) << "future should be ready immediately since we faked a "
299  "synchronous start with make_ready_future";
300  EXPECT_TRUE(fut.has_value());
301  auto reply = fut.get();
302  ASSERT_TRUE(reply);
303  EXPECT_EQ(reply->getId(), "id");
304 }
305 
306 TEST_F(TestOcmDaqService, StartDaqShouldFailIfAddDaqFails) {
307  // Setup
308  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).Times(Between(0, 1)).WillOnce(Return("file-id"));
309  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).WillOnce(Return(false));
310  EXPECT_CALL(m_mgr_mock, StartDaqAsync(_)).WillOnce(Throw(std::runtime_error("error")));
311 
312  // Run
313  auto fut = m_daq_impl->StartDaq("id", "prefix", "", m_metadata_sources, "");
314  m_io_ctx.poll();
315  ASSERT_TRUE(fut.is_ready()) << "future should be ready immediately since we faked a "
316  "synchronous start with make_ready_future";
317  EXPECT_THROW(fut.get(), daqif::DaqException)
318  << "future should have contained the ICD exception type";
319 }
320 
321 TEST_F(TestOcmDaqService, StartDaqShouldFailIfManagerStartFails) {
322  // Setup
323 
324  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).WillOnce(Return("fileid"));
325  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).WillOnce(Return(false));
326  EXPECT_CALL(m_mgr_mock, StartDaqAsync(Field(&daq::DaqContext::id, "id")))
327  .WillOnce(Return(ByMove(boost::make_exceptional_future<daq::State>(
328  daq::DaqSourceErrors({daq::DaqSourceError("start", "source", "message")})))));
329  EXPECT_CALL(m_mgr_mock, GetStatus("id"sv)).WillOnce(Return(daq::Status()));
330 
331  // Run
332  auto fut = m_daq_impl->StartDaq("id", "prefix", "", m_metadata_sources, "");
333  m_io_ctx.poll();
334  ASSERT_TRUE(fut.is_ready())
335  << "future should be ready now since we faked a synchronous start with make_ready_future";
336  EXPECT_THROW(fut.get(), daqif::DaqException)
337  << "future should have contained the ICD exception type";
338 }
339 
340 TEST_F(TestOcmDaqService, StartDaqV2ShouldCreateAndAssignIdIfNotProvided) {
341  // Setup
342  auto* fake_reply(new daqif::DaqReplyFake);
343 
344  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).WillOnce(Return("id"));
345  // Impl should check if id is already used.
346  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).WillOnce(Return(false));
347  EXPECT_CALL(m_mgr_mock, StartDaqAsync(Field(&daq::DaqContext::id, std::string("id"))))
348  .WillOnce(Return(ByMove(boost::make_ready_future<daq::State>(daq::State::Acquiring))));
349  // Have to construct and return void* and still keep a reference to it..
350  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
351  .WillOnce(Return(fake_reply))
352  .RetiresOnSaturation();
353 
354  // Run
355 
356  auto fut = m_daq_impl->StartDaqV2(R"(
357  {
358  "sources": [
359  {
360  "type": "metadataSource",
361  "sourceName": "name",
362  "rrUri": "uri"
363  }
364  ]
365  }
366  )");
367  // Requred to trigger any .then continuations, even though future is ready
368  m_io_ctx.poll();
369 
370  ASSERT_TRUE(fut.is_ready()) << "future should be ready immediately since we faked a "
371  "synchronous start with make_ready_future";
372  try {
373  EXPECT_TRUE(fut.has_value());
374  auto reply = fut.get();
375  ASSERT_TRUE(reply);
376  EXPECT_EQ(reply->getId(), "id");
377  } catch (daqif::DaqException const& e) {
378  FAIL() << e.getMessage();
379  } catch (std::exception const& e) {
380  FAIL() << e.what();
381  }
382 }
383 
384 TEST_F(TestOcmDaqService, StartDaqV2FailsIfJsonParsingFails) {
385  // Setup
386 
387  // Run
388  auto fut = m_daq_impl->StartDaqV2(",]{");
389  m_io_ctx.poll();
390  ASSERT_TRUE(fut.is_ready()) << "future should be ready now as parsing failed";
391  EXPECT_THROW(fut.get(), daqif::DaqException)
392  << "future should have contained the ICD exception type";
393 }
394 
395 TEST_F(TestOcmDaqService, StartDaqV2FailsIfSchemaParsingFails) {
396  // Setup
397 
398  // Run
399  auto fut = m_daq_impl->StartDaqV2("{}");
400  m_io_ctx.poll();
401  ASSERT_TRUE(fut.is_ready()) << "future should be ready now as schema validation failed";
402  EXPECT_THROW(fut.get(), daqif::DaqException)
403  << "future should have contained the ICD exception type";
404 }
405 
406 TEST_F(TestOcmDaqService, StartDaqV2ShouldAddAndStartDaqControllerIfArgumentsAreOk) {
407  // Setup
408  auto* fake_reply(new daqif::DaqReplyFake);
409 
410  // MakeDaqId is used for file-id anyway
411  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).WillOnce(Return("file-id"));
412  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).WillOnce(Return(false));
413  EXPECT_CALL(m_mgr_mock,
414  StartDaqAsync(AllOf(Field(&daq::DaqContext::id, "id"),
415  Field(&daq::DaqContext::file_id, "file-id"))))
416  .WillOnce(Return(ByMove(boost::make_ready_future<daq::State>(daq::State::Acquiring))));
417  // Have to construct and return void* and still keep a reference to it..
418  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
419  .WillOnce(Return(fake_reply))
420  .RetiresOnSaturation();
421 
422  // Run
423  auto fut = m_daq_impl->StartDaqV2(m_daqv2_spec);
424 
425  // Requred to trigger any .then continuations, even though future is ready
426  m_io_ctx.poll();
427 
428  ASSERT_TRUE(fut.is_ready()) << "future should be ready immediately since we faked a "
429  "synchronous start with make_ready_future";
430  EXPECT_TRUE(fut.has_value());
431  auto reply = fut.get();
432  ASSERT_TRUE(reply);
433  EXPECT_EQ(reply->getId(), "id");
434 }
435 
436 TEST_F(TestOcmDaqService, StopDaqShouldFailIfDaqDoesNotExist) {
437  // Setup
438  // Implementation might, or might not check if ID exist before issuing request to stop
439  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(false));
440  // Implementation might, or might not stop directly, leaving the id check to mgr
441  EXPECT_CALL(m_mgr_mock, StopDaqAsync("id"sv, daq::ErrorPolicy::Strict))
442  .Times(Between(0, 1))
443  .WillOnce(Return(ByMove(
444  boost::make_exceptional_future<daq::Status>(std::invalid_argument("no such id")))));
445 
446  EXPECT_CALL(m_mgr_mock, GetStatus("id"sv)).WillOnce(Throw(std::invalid_argument("no such id")));
447 
448  // Run
449  auto reply_fut = m_daq_impl->StopDaq("id");
450  m_io_ctx.poll();
451  ASSERT_TRUE(reply_fut.is_ready());
452  EXPECT_THROW(reply_fut.get(), daqif::DaqException);
453 }
454 
455 TEST_F(TestOcmDaqService, StopDaqShouldSucceedIfMgrOpSuceeds) {
456  // Setup
457  auto* fake_reply(new daqif::DaqReplyFake); // Note: Raw pointer since MAL use unsafe APIs
458  daq::Status status("id", "fileid", daq::State::Stopped, false, daq::Status::TimePoint());
459 
460  // Implementation might, or might not check if ID exist before issuing request to stop
461  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
462  EXPECT_CALL(m_mgr_mock, StopDaqAsync("id"sv, daq::ErrorPolicy::Strict))
463  .WillOnce(Return(ByMove(boost::make_ready_future<daq::Status>(status))));
464  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
465  .WillOnce(Return(fake_reply))
466  .RetiresOnSaturation();
467 
468  // Run
469  auto reply_fut = m_daq_impl->StopDaq("id");
470  m_io_ctx.poll();
471  ASSERT_TRUE(reply_fut.is_ready());
472  std::shared_ptr<daqif::DaqReply> reply = reply_fut.get();
473  EXPECT_EQ(reply->getId(), "id");
474 }
475 
476 TEST_F(TestOcmDaqService, ForceStopDaqShouldSucceedIfMgrOpSuceeds) {
477  // Setup
478  auto* fake_reply(new daqif::DaqReplyFake); // Note: Raw pointer since MAL use unsafe APIs
479  daq::Status status("id", "fileid", daq::State::Stopped, false, daq::Status::TimePoint());
480 
481  // Implementation might, or might not check if ID exist before issuing request to stop
482  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
483  EXPECT_CALL(m_mgr_mock, StopDaqAsync("id"sv, daq::ErrorPolicy::Tolerant))
484  .WillOnce(Return(ByMove(boost::make_ready_future<daq::Status>(status))));
485  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
486  .WillOnce(Return(fake_reply))
487  .RetiresOnSaturation();
488 
489  // Run
490  auto reply_fut = m_daq_impl->ForceStopDaq("id");
491  m_io_ctx.poll();
492  ASSERT_TRUE(reply_fut.is_ready());
493  std::shared_ptr<daqif::DaqReply> reply = reply_fut.get();
494  EXPECT_EQ(reply->getId(), "id");
495 }
496 
497 TEST_F(TestOcmDaqService, AbortDaqShouldFailIfDaqDoesNotExist) {
498  // Setup
499  // Implementation might, or might not check if ID exist before issuing request to abort
500  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(false));
501  EXPECT_CALL(m_mgr_mock, AbortDaqAsync("id"sv, daq::ErrorPolicy::Strict))
502  .Times(Between(0, 1))
503  .WillOnce(Return(ByMove(
504  boost::make_exceptional_future<daq::Status>(std::invalid_argument("no such id")))));
505 
506  EXPECT_CALL(m_mgr_mock, GetStatus("id"sv)).WillOnce(Throw(std::invalid_argument("no such id")));
507 
508  // Run
509  auto reply_fut = m_daq_impl->AbortDaq("id");
510  m_io_ctx.poll();
511  ASSERT_TRUE(reply_fut.is_ready());
512  EXPECT_THROW(reply_fut.get(), daqif::DaqException);
513 }
514 
515 TEST_F(TestOcmDaqService, AbortDaqShouldSucceedIfMgrOpSuceeds) {
516  // Setup
517  auto* fake_reply(new daqif::DaqReplyFake); // Note: Raw pointer since MAL use unsafe APIs
518 
519  auto reply_status = daq::Status("id", "fileid");
520  reply_status.state = daq::State::Aborted;
521 
522  // Implementation might, or might not check if ID exist before issuing request to stop
523  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
524  EXPECT_CALL(m_mgr_mock, AbortDaqAsync("id"sv, daq::ErrorPolicy::Strict))
525  .WillOnce(Return(ByMove(boost::make_ready_future<daq::Status>(reply_status))));
526  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
527  .WillOnce(Return(fake_reply))
528  .RetiresOnSaturation();
529 
530  // Run
531  auto reply_fut = m_daq_impl->AbortDaq("id");
532  m_io_ctx.poll();
533  ASSERT_TRUE(reply_fut.is_ready());
534  std::shared_ptr<daqif::DaqReply> reply = reply_fut.get();
535  EXPECT_EQ(reply->getId(), "id");
536 }
537 
538 TEST_F(TestOcmDaqService, ForceAbortDaqShouldUseTolerantPolicy) {
539  // Setup
540  auto* fake_reply(new daqif::DaqReplyFake); // Note: Raw pointer since MAL use unsafe APIs
541  auto reply_status = daq::Status("id", "fileid");
542  reply_status.state = daq::State::Aborted;
543 
544  // Implementation might, or might not check if ID exist before issuing request to stop
545  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
546  EXPECT_CALL(m_mgr_mock, AbortDaqAsync("id"sv, daq::ErrorPolicy::Tolerant))
547  .WillOnce(Return(ByMove(boost::make_ready_future<daq::Status>(reply_status))));
548  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
549  .WillOnce(Return(fake_reply))
550  .RetiresOnSaturation();
551 
552  // Run
553  auto reply_fut = m_daq_impl->ForceAbortDaq("id");
554  m_io_ctx.poll();
555  ASSERT_TRUE(reply_fut.is_ready());
556  std::shared_ptr<daqif::DaqReply> reply = reply_fut.get();
557  EXPECT_EQ(reply->getId(), "id");
558 }
559 
560 TEST_F(TestOcmDaqService, UpdateKeywordsShouldSucceedIfMgrOpSucceeds) {
561  // Setup
562  std::string keywords = R"(
563  [
564  {
565  "type":"valueKeyword",
566  "name":"OBJECT",
567  "value":"OBJECT,SKY"
568  },
569  {
570  "type":"esoKeyword",
571  "name":"OBS TPLNO",
572  "value":2
573  }
574  ]
575  )";
576  // This is what `keywords` should contain when parsed.
577  daq::fits::KeywordVector parsed_keywords = {
578  daq::fits::ValueKeyword("OBJECT", "OBJECT,SKY"),
579  daq::fits::EsoKeyword("OBS TPLNO", static_cast<uint64_t>(2))};
580  auto* fake_reply(new daqif::DaqReplyFake); // Note: Raw pointer since MAL use unsafe APIs
581  // Implementation might, or might not check if ID exist before issuing request to stop
582  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
583  EXPECT_CALL(m_mgr_mock, UpdateKeywords("id"sv, parsed_keywords));
584  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqReply"))
585  .WillOnce(Return(fake_reply))
586  .RetiresOnSaturation();
587 
588  // Run
589  auto reply_fut = m_daq_impl->UpdateKeywords("id", keywords);
590  m_io_ctx.poll();
591  ASSERT_TRUE(reply_fut.is_ready());
592  std::shared_ptr<daqif::DaqReply> reply = reply_fut.get();
593  EXPECT_EQ(reply->getId(), "id");
594  EXPECT_EQ(reply->getError(), false);
595 }
596 
597 TEST_F(TestOcmDaqService, UpdateKeywordsFailsIfJsonParseFails) {
598  // Setup
599  std::string keywords_with_trailing_comma = R"(
600  [
601  {
602  "type":"valueKeyword",
603  "name":"OBJECT",
604  "value":"OBJECT,SKY"
605  },
606  {
607  "type":"esoKeyword",
608  "name":"OBS TPLNO",
609  "value":2
610  },
611  ]
612  )";
613  // Implementation might, or might not check if ID exist before issuing request to stop
614  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
615  EXPECT_CALL(m_mgr_mock, UpdateKeywords("id"sv, _)).Times(0);
616 
617  // Run
618  auto reply_fut = m_daq_impl->UpdateKeywords("id", keywords_with_trailing_comma);
619  m_io_ctx.poll();
620  ASSERT_TRUE(reply_fut.is_ready());
621  EXPECT_THROW(reply_fut.get(), daqif::DaqException);
622 }
623 
624 TEST_F(TestOcmDaqService, UpdateKeywordsFailsIfSchemaParsingFails) {
625  // Setup
626  std::string keywords_with_unknown_type = R"(
627  [
628  {
629  "type":"unknownKeywordHere",
630  "name":"OBJECT",
631  "value":"OBJECT,SKY"
632  }
633  ]
634  )";
635  // Implementation might, or might not check if ID exist before issuing request to stop
636  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
637  EXPECT_CALL(m_mgr_mock, UpdateKeywords("id"sv, _)).Times(0);
638 
639  // Run
640  auto reply_fut = m_daq_impl->UpdateKeywords("id", keywords_with_unknown_type);
641  m_io_ctx.poll();
642  ASSERT_TRUE(reply_fut.is_ready());
643  EXPECT_THROW(reply_fut.get(), daqif::DaqException);
644 }
645 
646 TEST_F(TestOcmDaqService, GetStatusFailsIfDaqDoesNotExist) {
647  // Setup
648  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(false));
649  EXPECT_CALL(m_mgr_mock, GetStatus("id"sv))
650  .Times(Between(0, 1))
651  .WillOnce(Throw(std::invalid_argument("no such id")));
652 
653  // Run
654  auto reply_fut = m_daq_impl->GetStatus("id");
655  EXPECT_FALSE(reply_fut.is_ready())
656  << "future cannot be ready since implementation should use provided executor to provide "
657  "thread safety";
658  m_io_ctx.poll();
659  ASSERT_TRUE(reply_fut.is_ready());
660  EXPECT_THROW(reply_fut.get(), daqif::DaqException);
661 }
662 
663 TEST_F(TestOcmDaqService, GetStatusSuccedsIfMgrOpSucceeds) {
664  // Setup
665  daq::Status status("id", "fileid", daq::State::Acquiring, false, daq::Status::TimePoint());
666  auto* fake_reply(new daqif::DaqStatusFake); // Note: Raw pointer since MAL use unsafe APIs
667 
668  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
669  EXPECT_CALL(m_mgr_mock, GetStatus("id"sv)).Times(Between(0, 1)).WillOnce(Return(status));
670  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqStatus"))
671  .WillOnce(Return(fake_reply))
672  .RetiresOnSaturation();
673 
674  // Run
675  auto reply_fut = m_daq_impl->GetStatus("id");
676  EXPECT_FALSE(reply_fut.is_ready())
677  << "future cannot be ready since implementation should use provided executor to provide "
678  "thread safety";
679  m_io_ctx.poll();
680  ASSERT_TRUE(reply_fut.is_ready());
681  std::shared_ptr<daqif::DaqStatus> reply = reply_fut.get();
682  EXPECT_EQ(reply->getId(), "id");
683  EXPECT_EQ(reply->getFileId(), "fileid");
684  EXPECT_EQ(reply->getError(), false);
685  EXPECT_EQ(reply->getState(), daqif::StateAcquiring);
686  EXPECT_EQ(reply->getSubState(), daqif::Acquiring);
687 }
688 
689 TEST_F(TestOcmDaqService, GetActiveReturnsActive) {
690  // Setup
691  auto status1 = std::make_shared<daq::ObservableStatus>("completed", "fileid1");
692  auto status2 = std::make_shared<daq::ObservableStatus>("active1", "fileid2");
693  auto status3 = std::make_shared<daq::ObservableStatus>("active2", "fileid3");
694  auto status4 = std::make_shared<daq::ObservableStatus>("completed", "fileid4");
695  // Make DAQ completed
696  status1->SetState(daq::State::Aborted);
697  status4->SetState(daq::State::Aborted);
698 
699  auto daq1 = std::make_shared<DaqControllerFake>(status1);
700  auto daq2 = std::make_shared<DaqControllerFake>(status2);
701  auto daq3 = std::make_shared<DaqControllerFake>(status3);
702  auto daq4 = std::make_shared<DaqControllerFake>(status4);
703  std::vector<std::shared_ptr<daq::DaqController const>> daqs;
704  daqs.push_back(daq1);
705  daqs.push_back(daq2);
706  daqs.push_back(daq3);
707  daqs.push_back(daq4);
708 
709  auto* st1 = new daqif::DaqStatusFake;
710  auto* st2 = new daqif::DaqStatusFake;
711 
712  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_DaqStatus"))
713  .WillOnce(Return(st1))
714  .WillOnce(Return(st2))
715  .RetiresOnSaturation();
716 
717  EXPECT_CALL(m_mgr_mock, GetDaqControllers()).WillOnce(Return(daqs));
718 
719  // Run
720  auto reply_fut = m_daq_impl->GetActiveList();
721  m_io_ctx.poll();
722  ASSERT_TRUE(reply_fut.is_ready());
723  auto reply = reply_fut.get();
724  EXPECT_EQ(reply.size(), 2u);
725  EXPECT_EQ(reply[0]->getId(), "active1");
726  EXPECT_EQ(reply[1]->getId(), "active2");
727 }
728 
729 TEST_F(TestOcmDaqService, AwaitDaqstateFailsWithInvalidArguments) {
730  // Setup
731  EXPECT_CALL(m_mgr_mock, GetStatus("id"sv)).WillRepeatedly(Return(daq::Status()));
732 
733  // Run
734  {
735  // Invalid timeout
736  auto fut = m_daq_impl->AwaitDaqState("id", daqif::StateAcquiring, daqif::Acquiring, 0.0);
737  m_io_ctx.poll();
738  ASSERT_TRUE(fut.is_ready());
739  EXPECT_THROW(fut.get(), daqif::DaqException);
740  }
741  m_io_ctx.reset();
742  {
743  // Invalid state combination
744  auto fut = m_daq_impl->AwaitDaqState("id", daqif::StateMerging, daqif::Acquiring, 1.0);
745  m_io_ctx.poll();
746  ASSERT_TRUE(fut.is_ready());
747  EXPECT_THROW(fut.get(), daqif::DaqException);
748  }
749 }
750 
751 TEST_F(TestOcmDaqService, AwaitDaqstateSucceds) {
752  // Setup
753  daq::Status status("id", "fileid");
754  status.state = daq::State::Acquiring;
755 
756  boost::promise<daq::Result<daq::Status>> promise;
757  EXPECT_CALL(m_mgr_mock, AwaitDaqStateAsync("id"sv, daq::State::Acquiring, 1000ms))
758  .WillOnce(Return(ByMove(promise.get_future())));
759 
760  auto* reply = new daqif::AwaitDaqReplyFake;
761 
762  EXPECT_CALL(m_mal_mock, getDataEntityUnsafe(_, "daqif_AwaitDaqReply"))
763  .WillOnce(Return(reply))
764  .RetiresOnSaturation();
765 
766  // Run
767  auto fut = m_daq_impl->AwaitDaqState("id", daqif::StateAcquiring, daqif::Acquiring, 1.0);
768  ASSERT_FALSE(fut.is_ready());
769  promise.set_value({false, status});
770  m_io_ctx.poll();
771  {
772  auto val = fut.get();
773  EXPECT_FALSE(val->getTimeout());
774  auto status = val->getStatus();
775  EXPECT_EQ(status->getId(), "id");
776  EXPECT_EQ(status->getError(), false);
777  EXPECT_EQ(status->getState(), daqif::StateAcquiring);
778  EXPECT_EQ(status->getSubState(), daqif::Acquiring);
779  }
780 }
781 
783  // Setup
784 
785  // This is the promise used to create result from daq::Manager
786  boost::promise<daq::State> mgr_promise;
787  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).WillOnce(Return(false));
788  EXPECT_CALL(m_mgr_mock, MakeDaqId(_)).WillOnce(Return("fileid"));
789  EXPECT_CALL(m_mgr_mock, StartDaqAsync(_)).WillOnce(Return(ByMove(mgr_promise.get_future())));
790 
791  // Run
792  auto reply_fut = m_daq_impl->StartDaq("id", "prefix", m_prim_sources, m_metadata_sources, "");
793  // Abandon the service
794  m_daq_impl.reset();
795 
796  // Fulfull promise from daq::Manager:
797  mgr_promise.set_value(daq::State::Acquiring);
798 
799  // Trigger handlers
800  m_io_ctx.poll();
801  ASSERT_TRUE(reply_fut.is_ready());
802  EXPECT_THROW(reply_fut.get(), daqif::DaqException);
803 }
804 
806  // Setup
807 
808  // This is the promise used to create result from daq::Manager
809  boost::promise<daq::Status> mgr_promise;
810 
811  // Implementation might, or might not check if ID exist before issuing request to stop
812  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
813  EXPECT_CALL(m_mgr_mock, StopDaqAsync("id"sv, daq::ErrorPolicy::Strict))
814  .WillOnce(Return(ByMove(mgr_promise.get_future())));
815 
816  // Run
817  auto reply_fut = m_daq_impl->StopDaq("id");
818  // Abandon the service
819  m_daq_impl.reset();
820 
821  // Fulfull promise from daq::Manager:
822  mgr_promise.set_value(
823  daq::Status("id", "fileid", daq::State::Stopped, true, daq::Status::TimePoint()));
824 
825  // Trigger handlers
826  m_io_ctx.poll();
827  ASSERT_TRUE(reply_fut.is_ready());
828  EXPECT_THROW(reply_fut.get(), daqif::DaqException);
829 }
830 
832  // Setup
833 
834  // This is the promise used to create result from daq::Manager
835  boost::promise<daq::Status> mgr_promise;
836 
837  auto reply_status = daq::Status("id", "fileid");
838  reply_status.state = daq::State::Aborted;
839 
840  // Implementation might, or might not check if ID exist before issuing request to stop
841  EXPECT_CALL(m_mgr_mock, HaveDaq("id"sv, _)).Times(Between(0, 1)).WillRepeatedly(Return(true));
842  EXPECT_CALL(m_mgr_mock, AbortDaqAsync("id"sv, daq::ErrorPolicy::Strict))
843  .WillOnce(Return(ByMove(mgr_promise.get_future())));
844 
845  // Run
846  auto reply_fut = m_daq_impl->AbortDaq("id");
847  // Abandon the service
848  m_daq_impl.reset();
849 
850  // Fulfull promise from daq::Manager:
851  mgr_promise.set_value(reply_status);
852 
853  // Trigger handlers
854  m_io_ctx.poll();
855  ASSERT_TRUE(reply_fut.is_ready());
856  EXPECT_THROW(reply_fut.get(), daqif::DaqException);
857 }
Represents error in single source.
Definition: error.hpp:67
Exception thrown to carry reply errors.
Definition: error.hpp:84
Contains error related declarations for DAQ.
Implements the MAL interface daqif::OcmDaq (async version).
Mockup of metadaqif classes.
std::vector< KeywordVariant > KeywordVector
Vector of keywords.
Definition: keyword.hpp:414
BasicKeyword< ValueKeywordTraits > ValueKeyword
Standard FITS value keyword.
Definition: keyword.hpp:330
BasicKeyword< EsoKeywordTraits > EsoKeyword
ESO hiearchical keyword.
Definition: keyword.hpp:337
void UpdateKeywords(DaqContext &ctx, fits::KeywordVector const &keywords)
Updates (adds or replaces) primary HDU keywords.
Definition: daqContext.cpp:29
@ Strict
Any error is considered fatal and may lead to the operation being aborted.
@ Tolerant
Errors that can be ignored with partial completion of a command will be tolerated and is reported as ...
@ Aborted
Data acquisition has been aborted by user.
@ Acquiring
All data sources have reported data acquisition is in progress.
@ Stopped
All data sources have reported they have stopped acquiring data.
daq::DaqContext ParseStartDaqContext(std::string const &json_properties)
Parse the JSON properties user provides with StartDaq.
ParsedSource ParseSourceUri(std::string_view s)
Parse user provided string in the format "<name>@<rr-uri>".
std::vector< ParsedSource > ParseSourceUris(std::string_view s)
Parse user provided string in the format "<name>@<rr-uri>[ <name>@...]".
Declaration of OcmDaqService.
Mockup of metadaqif classes.
std::string name
std::string rr_uri
Fixture for testing when OcmDaqService is abandoned (e.g.
Fixture that sets up a OcmDaqService instance with following mockups:
std::shared_ptr< daq::ObservableEventLog > m_event_log
std::string m_metadata_sources
boost::asio::io_context m_io_ctx
std::shared_ptr< OcmDaqService > m_daq_impl
std::string m_daq_properties
void SetUp() override
std::string m_prim_sources
void TearDown() override
std::string file_id
Data Product FileId as specified by OLAS ICD.
Definition: daqContext.hpp:65
std::string id
DAQ identfier, possibly provided by user.
Definition: daqContext.hpp:60
Non observable status object that keeps stores status of data acquisition.
Definition: status.hpp:124
std::chrono::time_point< std::chrono::steady_clock > TimePoint
Definition: status.hpp:125
State state
Definition: status.hpp:142
TEST_F(TestOcmDaqService, StartDaqShouldTestIfIdAlreadyExistsAndReturnFailureIfItDoes)
TEST(TestParseSingleSource, Successful)
EXPECT_EQ(meta.rr_uri, "zpb.rr://meta")