blob: 816d4714c1947a8f386045ddf62c315554e37883 [file] [log] [blame]
Brad Bishop49aefb32016-10-19 11:54:14 -04001/**
2 * Copyright © 2016 IBM Corporation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Brad Bishope30dd772016-11-12 21:39:28 -050016#include "../manager.hpp"
Brad Bishop49aefb32016-10-19 11:54:14 -040017#include "../config.h"
18#include <cassert>
Brad Bishopabb2a1a2016-11-30 22:02:28 -050019#include <iostream>
20#include <algorithm>
Brad Bishop21621432017-01-13 16:35:53 -050021#include <thread>
Brad Bishop8f868502017-01-23 13:13:58 -050022#include <chrono>
23#include <xyz/openbmc_project/Example/Iface1/server.hpp>
24#include <xyz/openbmc_project/Example/Iface2/server.hpp>
Brad Bishop49aefb32016-10-19 11:54:14 -040025
Brad Bishop8f868502017-01-23 13:13:58 -050026using namespace std::literals::chrono_literals;
27using namespace std::literals::string_literals;
28
29constexpr auto MGR_SERVICE = "phosphor.inventory.test.mgr";
30constexpr auto MGR_INTERFACE = IFACE;
31constexpr auto MGR_ROOT = "/testing/inventory";
32constexpr auto EXAMPLE_SERVICE = "phosphor.inventory.test.example";
33constexpr auto EXAMPLE_ROOT = "/testing";
34
35const auto trigger1 = sdbusplus::message::object_path(EXAMPLE_ROOT +
36 "/trigger1"s);
37const auto trigger2 = sdbusplus::message::object_path(EXAMPLE_ROOT +
38 "/trigger2"s);
Brad Bishopfb083c22017-01-19 09:22:04 -050039const auto trigger3 = sdbusplus::message::object_path(EXAMPLE_ROOT +
40 "/trigger3"s);
Brad Bishopfa51da72017-01-19 11:06:51 -050041const auto trigger4 = sdbusplus::message::object_path(EXAMPLE_ROOT +
42 "/trigger4"s);
Brad Bishopeb68a682017-01-22 00:58:54 -050043const auto trigger5 = sdbusplus::message::object_path(EXAMPLE_ROOT +
44 "/trigger5"s);
Brad Bishopfb083c22017-01-19 09:22:04 -050045
46const sdbusplus::message::object_path relDeleteMeOne{"/deleteme1"};
47const sdbusplus::message::object_path relDeleteMeTwo{"/deleteme2"};
48const sdbusplus::message::object_path relDeleteMeThree{"/deleteme3"};
49
50const std::string root{MGR_ROOT};
51const std::string deleteMeOne{root + relDeleteMeOne.str};
52const std::string deleteMeTwo{root + relDeleteMeTwo.str};
53const std::string deleteMeThree{root + relDeleteMeThree.str};
Brad Bishop8f868502017-01-23 13:13:58 -050054
55using ExampleIface1 = sdbusplus::xyz::openbmc_project::Example::server::Iface1;
56using ExampleIface2 = sdbusplus::xyz::openbmc_project::Example::server::Iface2;
57
58/** @class ExampleService
59 * @brief Host an object for triggering events.
60 */
61struct ExampleService
62{
63 ~ExampleService() = default;
64 ExampleService() :
65 shutdown(false),
66 bus(sdbusplus::bus::new_default()),
67 objmgr(sdbusplus::server::manager::manager(bus, MGR_ROOT))
68 {
69 bus.request_name(EXAMPLE_SERVICE);
70 }
71
72 void run()
73 {
74 sdbusplus::server::object::object <
75 ExampleIface1, ExampleIface2 > t1(bus, trigger1.str.c_str());
76 sdbusplus::server::object::object <
77 ExampleIface1, ExampleIface2 > t2(bus, trigger2.str.c_str());
Brad Bishopfb083c22017-01-19 09:22:04 -050078 sdbusplus::server::object::object <
79 ExampleIface1, ExampleIface2 > t3(bus, trigger3.str.c_str());
Brad Bishopfa51da72017-01-19 11:06:51 -050080 sdbusplus::server::object::object <
81 ExampleIface1, ExampleIface2 > t4(bus, trigger4.str.c_str());
Brad Bishopeb68a682017-01-22 00:58:54 -050082 sdbusplus::server::object::object <
83 ExampleIface1, ExampleIface2 > t5(bus, trigger5.str.c_str());
Brad Bishop8f868502017-01-23 13:13:58 -050084
85 while (!shutdown)
86 {
87 bus.process_discard();
88 bus.wait((5000000us).count());
89 }
90 }
91
92 volatile bool shutdown;
93 sdbusplus::bus::bus bus;
94 sdbusplus::server::manager::manager objmgr;
95};
Brad Bishop49aefb32016-10-19 11:54:14 -040096
Brad Bishop1157af12017-01-22 01:03:02 -050097using Object = phosphor::inventory::manager::Object;
98
Brad Bishopabb2a1a2016-11-30 22:02:28 -050099/** @class SignalQueue
100 * @brief Store DBus signals in a queue.
101 */
102class SignalQueue
103{
104 public:
Brad Bishop7b337772017-01-12 16:11:24 -0500105 ~SignalQueue() = default;
106 SignalQueue() = delete;
107 SignalQueue(const SignalQueue&) = delete;
108 SignalQueue(SignalQueue&&) = default;
109 SignalQueue& operator=(const SignalQueue&) = delete;
110 SignalQueue& operator=(SignalQueue&&) = default;
111 explicit SignalQueue(const std::string& match) :
112 _bus(sdbusplus::bus::new_default()),
113 _match(_bus, match.c_str(), &callback, this),
114 _next(nullptr)
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500115 {
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500116 }
Brad Bishop7b337772017-01-12 16:11:24 -0500117
118 auto&& pop(unsigned timeout = 1000000)
119 {
120 while (timeout > 0 && !_next)
121 {
122 _bus.process_discard();
123 _bus.wait(50000);
124 timeout -= 50000;
125 }
126 return std::move(_next);
127 }
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500128
129 private:
Brad Bishop7b337772017-01-12 16:11:24 -0500130 static int callback(sd_bus_message* m, void* context, sd_bus_error*)
131 {
132 auto* me = static_cast<SignalQueue*>(context);
133 sd_bus_message_ref(m);
134 sdbusplus::message::message msg{m};
135 me->_next = std::move(msg);
136 return 0;
137 }
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500138
Brad Bishop7b337772017-01-12 16:11:24 -0500139 sdbusplus::bus::bus _bus;
140 sdbusplus::server::match::match _match;
141 sdbusplus::message::message _next;
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500142};
143
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500144/**@brief Find a subset of interfaces and properties in an object. */
Brad Bishop1157af12017-01-22 01:03:02 -0500145auto hasProperties(const Object& l, const Object& r)
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500146{
Brad Bishop1157af12017-01-22 01:03:02 -0500147 Object result;
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500148 std::set_difference(
Brad Bishop7b337772017-01-12 16:11:24 -0500149 r.cbegin(),
150 r.cend(),
151 l.cbegin(),
152 l.cend(),
153 std::inserter(result, result.end()));
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500154 return result.empty();
155}
156
Brad Bishop432e3522016-12-01 00:24:14 -0500157/**@brief Check an object for one or more interfaces. */
Brad Bishop1157af12017-01-22 01:03:02 -0500158auto hasInterfaces(const std::vector<std::string>& l, const Object& r)
Brad Bishop432e3522016-12-01 00:24:14 -0500159{
160 std::vector<std::string> stripped, interfaces;
161 std::transform(
Brad Bishop7b337772017-01-12 16:11:24 -0500162 r.cbegin(),
163 r.cend(),
164 std::back_inserter(stripped),
165 [](auto & p)
166 {
167 return p.first;
168 });
Brad Bishop432e3522016-12-01 00:24:14 -0500169 std::set_difference(
Brad Bishop7b337772017-01-12 16:11:24 -0500170 stripped.cbegin(),
171 stripped.cend(),
172 l.cbegin(),
173 l.cend(),
174 std::back_inserter(interfaces));
Brad Bishop432e3522016-12-01 00:24:14 -0500175 return interfaces.empty();
176}
177
Brad Bishop8f868502017-01-23 13:13:58 -0500178void runTests()
Brad Bishop49aefb32016-10-19 11:54:14 -0400179{
Brad Bishop8f868502017-01-23 13:13:58 -0500180 const std::string exampleRoot{EXAMPLE_ROOT};
Brad Bishop49aefb32016-10-19 11:54:14 -0400181 auto b = sdbusplus::bus::new_default();
Brad Bishop8f868502017-01-23 13:13:58 -0500182
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500183 auto notify = [&]()
Brad Bishop49aefb32016-10-19 11:54:14 -0400184 {
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500185 return b.new_method_call(
Brad Bishop8f868502017-01-23 13:13:58 -0500186 MGR_SERVICE,
187 MGR_ROOT,
188 MGR_INTERFACE,
Brad Bishop7b337772017-01-12 16:11:24 -0500189 "Notify");
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500190 };
Brad Bishop7b337772017-01-12 16:11:24 -0500191 auto set = [&](const std::string & path)
Brad Bishop432e3522016-12-01 00:24:14 -0500192 {
193 return b.new_method_call(
Brad Bishop8f868502017-01-23 13:13:58 -0500194 EXAMPLE_SERVICE,
Brad Bishop7b337772017-01-12 16:11:24 -0500195 path.c_str(),
196 "org.freedesktop.DBus.Properties",
197 "Set");
Brad Bishop432e3522016-12-01 00:24:14 -0500198 };
Brad Bishop49aefb32016-10-19 11:54:14 -0400199
Brad Bishop1157af12017-01-22 01:03:02 -0500200 Object obj
Brad Bishop7b337772017-01-12 16:11:24 -0500201 {
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500202 {
203 "xyz.openbmc_project.Example.Iface1",
204 {{"ExampleProperty1", "test1"}}
205 },
206 {
207 "xyz.openbmc_project.Example.Iface2",
208 {{"ExampleProperty2", "test2"}}
209 },
210 };
Brad Bishop49aefb32016-10-19 11:54:14 -0400211
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500212 // Make sure the notify method works.
213 {
Brad Bishop9aa5e2f2017-01-15 19:45:40 -0500214 sdbusplus::message::object_path relPath{"/foo"};
215 std::string path(root + relPath.str);
Brad Bishop49aefb32016-10-19 11:54:14 -0400216
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500217 SignalQueue queue(
Brad Bishop7b337772017-01-12 16:11:24 -0500218 "path='" + root + "',member='InterfacesAdded'");
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500219
220 auto m = notify();
221 m.append(relPath);
222 m.append(obj);
223 b.call(m);
224
225 auto sig{queue.pop()};
226 assert(sig);
Brad Bishop9aa5e2f2017-01-15 19:45:40 -0500227 sdbusplus::message::object_path signalPath;
Brad Bishop1157af12017-01-22 01:03:02 -0500228 Object signalObjectType;
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500229 sig.read(signalPath);
Brad Bishope07a1642017-01-24 14:37:33 -0500230 assert(path == signalPath.str);
Brad Bishop1157af12017-01-22 01:03:02 -0500231 sig.read(signalObjectType);
232 assert(hasProperties(signalObjectType, obj));
Brad Bishopabb2a1a2016-11-30 22:02:28 -0500233 auto moreSignals{queue.pop()};
234 assert(!moreSignals);
Brad Bishop49aefb32016-10-19 11:54:14 -0400235 }
236
Brad Bishopfa51da72017-01-19 11:06:51 -0500237 // Validate the propertyIs filter.
238 {
239 // Create an object to be deleted.
240 {
241 auto m = notify();
242 m.append(relDeleteMeThree);
243 m.append(obj);
244 b.call(m);
245 }
246
247 // Validate that the action does not run if the property doesn't match.
248 {
249 SignalQueue queue(
250 "path='" + root + "',member='InterfacesRemoved'");
251 auto m = set(trigger4.str);
252 m.append("xyz.openbmc_project.Example.Iface2");
253 m.append("ExampleProperty2");
254 m.append(sdbusplus::message::variant<std::string>("123"));
255 b.call(m);
256 auto sig{queue.pop()};
257 assert(!sig);
258 }
259
260 // Validate that the action does run if the property matches.
261 {
262 // Set ExampleProperty2 to something else to the 123 filter
263 // matches.
264 SignalQueue queue(
265 "path='" + root + "',member='InterfacesRemoved'");
266 auto m = set(trigger4.str);
267 m.append("xyz.openbmc_project.Example.Iface2");
268 m.append("ExampleProperty2");
269 m.append(sdbusplus::message::variant<std::string>("xyz"));
270 b.call(m);
271 auto sig{queue.pop()};
272 assert(!sig);
273 }
274 {
275 // Set ExampleProperty3 to 99.
276 SignalQueue queue(
277 "path='" + root + "',member='InterfacesRemoved'");
278 auto m = set(trigger4.str);
279 m.append("xyz.openbmc_project.Example.Iface2");
280 m.append("ExampleProperty3");
281 m.append(sdbusplus::message::variant<int64_t>(99));
282 b.call(m);
283 auto sig{queue.pop()};
284 assert(!sig);
285 }
286 {
287 SignalQueue queue(
288 "path='" + root + "',member='InterfacesRemoved'");
289 auto m = set(trigger4.str);
290 m.append("xyz.openbmc_project.Example.Iface2");
291 m.append("ExampleProperty2");
292 m.append(sdbusplus::message::variant<std::string>("123"));
293 b.call(m);
294
295 sdbusplus::message::object_path sigpath;
296 std::vector<std::string> interfaces;
297 {
298 std::vector<std::string> interfaces;
299 auto sig{queue.pop()};
300 assert(sig);
301 sig.read(sigpath);
302 assert(sigpath == deleteMeThree);
303 sig.read(interfaces);
304 std::sort(interfaces.begin(), interfaces.end());
305 assert(hasInterfaces(interfaces, obj));
306 }
307 }
308 }
309
Brad Bishop432e3522016-12-01 00:24:14 -0500310 // Make sure DBus signals are handled.
311 {
Brad Bishop432e3522016-12-01 00:24:14 -0500312 // Create some objects to be deleted by an action.
313 {
314 auto m = notify();
315 m.append(relDeleteMeOne);
316 m.append(obj);
317 b.call(m);
318 }
319 {
320 auto m = notify();
321 m.append(relDeleteMeTwo);
322 m.append(obj);
323 b.call(m);
324 }
Brad Bishopfb083c22017-01-19 09:22:04 -0500325 {
326 auto m = notify();
327 m.append(relDeleteMeThree);
328 m.append(obj);
329 b.call(m);
330 }
Brad Bishop432e3522016-12-01 00:24:14 -0500331
Brad Bishopfb083c22017-01-19 09:22:04 -0500332 // Set some properties that should not trigger due to a filter.
Brad Bishop432e3522016-12-01 00:24:14 -0500333 {
334 SignalQueue queue(
Brad Bishop7b337772017-01-12 16:11:24 -0500335 "path='" + root + "',member='InterfacesRemoved'");
Brad Bishop8f868502017-01-23 13:13:58 -0500336 auto m = set(trigger1.str);
Brad Bishop432e3522016-12-01 00:24:14 -0500337 m.append("xyz.openbmc_project.Example.Iface2");
338 m.append("ExampleProperty2");
339 m.append(sdbusplus::message::variant<std::string>("abc123"));
340 b.call(m);
341 auto sig{queue.pop()};
342 assert(!sig);
343 }
Brad Bishopfb083c22017-01-19 09:22:04 -0500344 {
345 SignalQueue queue(
346 "path='" + root + "',member='InterfacesRemoved'");
347 auto m = set(trigger3.str);
348 m.append("xyz.openbmc_project.Example.Iface2");
349 m.append("ExampleProperty3");
350 m.append(sdbusplus::message::variant<int64_t>(11));
351 b.call(m);
352 auto sig{queue.pop()};
353 assert(!sig);
354 }
Brad Bishop432e3522016-12-01 00:24:14 -0500355
Brad Bishopfb083c22017-01-19 09:22:04 -0500356 // Set some properties that should trigger.
Brad Bishop432e3522016-12-01 00:24:14 -0500357 {
358 SignalQueue queue(
Brad Bishop7b337772017-01-12 16:11:24 -0500359 "path='" + root + "',member='InterfacesRemoved'");
Brad Bishop432e3522016-12-01 00:24:14 -0500360
Brad Bishop8f868502017-01-23 13:13:58 -0500361 auto m = set(trigger1.str);
Brad Bishop432e3522016-12-01 00:24:14 -0500362 m.append("xyz.openbmc_project.Example.Iface2");
363 m.append("ExampleProperty2");
364 m.append(sdbusplus::message::variant<std::string>("xxxyyy"));
365 b.call(m);
366
Brad Bishop9aa5e2f2017-01-15 19:45:40 -0500367 sdbusplus::message::object_path sigpath;
Brad Bishop432e3522016-12-01 00:24:14 -0500368 std::vector<std::string> interfaces;
369 {
370 std::vector<std::string> interfaces;
371 auto sig{queue.pop()};
372 assert(sig);
373 sig.read(sigpath);
374 assert(sigpath == deleteMeOne);
375 sig.read(interfaces);
376 std::sort(interfaces.begin(), interfaces.end());
377 assert(hasInterfaces(interfaces, obj));
378 }
379 {
380 std::vector<std::string> interfaces;
381 auto sig{queue.pop()};
382 assert(sig);
383 sig.read(sigpath);
384 assert(sigpath == deleteMeTwo);
385 sig.read(interfaces);
386 std::sort(interfaces.begin(), interfaces.end());
387 assert(hasInterfaces(interfaces, obj));
388 }
389 {
390 // Make sure there were only two signals.
391 auto sig{queue.pop()};
392 assert(!sig);
393 }
394 }
Brad Bishopfb083c22017-01-19 09:22:04 -0500395 {
396 SignalQueue queue(
397 "path='" + root + "',member='InterfacesRemoved'");
398
399 auto m = set(trigger3.str);
400 m.append("xyz.openbmc_project.Example.Iface2");
401 m.append("ExampleProperty3");
402 m.append(sdbusplus::message::variant<int64_t>(10));
403 b.call(m);
404
405 sdbusplus::message::object_path sigpath;
406 std::vector<std::string> interfaces;
407 {
408 std::vector<std::string> interfaces;
409 auto sig{queue.pop()};
410 assert(sig);
411 sig.read(sigpath);
412 assert(sigpath == deleteMeThree);
413 sig.read(interfaces);
414 std::sort(interfaces.begin(), interfaces.end());
415 assert(hasInterfaces(interfaces, obj));
416 }
417 {
418 // Make sure there was only one signal.
419 auto sig{queue.pop()};
420 assert(!sig);
421 }
422 }
Brad Bishop432e3522016-12-01 00:24:14 -0500423 }
424
Brad Bishop22ecacc2016-12-01 08:38:06 -0500425 // Validate the set property action.
426 {
Brad Bishop9aa5e2f2017-01-15 19:45:40 -0500427 sdbusplus::message::object_path relChangeMe{"/changeme"};
Brad Bishop9aa5e2f2017-01-15 19:45:40 -0500428 std::string changeMe{root + relChangeMe.str};
Brad Bishop22ecacc2016-12-01 08:38:06 -0500429
430 // Create an object to be updated by the set property action.
431 {
432 auto m = notify();
433 m.append(relChangeMe);
434 m.append(obj);
435 b.call(m);
436 }
437
Brad Bishop22ecacc2016-12-01 08:38:06 -0500438 // Trigger and validate the change.
439 {
440 SignalQueue queue(
Brad Bishop7b337772017-01-12 16:11:24 -0500441 "path='" + changeMe + "',member='PropertiesChanged'");
Brad Bishop8f868502017-01-23 13:13:58 -0500442 auto m = set(trigger2.str);
Brad Bishop22ecacc2016-12-01 08:38:06 -0500443 m.append("xyz.openbmc_project.Example.Iface2");
444 m.append("ExampleProperty2");
445 m.append(sdbusplus::message::variant<std::string>("yyyxxx"));
446 b.call(m);
447
448 std::string sigInterface;
Brad Bishop7b337772017-01-12 16:11:24 -0500449 std::map <
450 std::string,
451 sdbusplus::message::variant<std::string >> sigProperties;
Brad Bishop22ecacc2016-12-01 08:38:06 -0500452 {
453 std::vector<std::string> interfaces;
454 auto sig{queue.pop()};
455 sig.read(sigInterface);
456 assert(sigInterface == "xyz.openbmc_project.Example.Iface1");
457 sig.read(sigProperties);
458 assert(sigProperties["ExampleProperty1"] == "changed");
459 }
460 }
461 }
Brad Bishopeb68a682017-01-22 00:58:54 -0500462
463 // Validate the create object action.
464 {
465 sdbusplus::message::object_path relCreateMe1{"/createme1"};
466 sdbusplus::message::object_path relCreateMe2{"/createme2"};
467 std::string createMe1{root + relCreateMe1.str};
468 std::string createMe2{root + relCreateMe2.str};
469
470 // Trigger the action.
471 {
472 sdbusplus::message::object_path signalPath;
473 Object signalObject;
474
475 SignalQueue queue(
476 "path='" + root + "',member='InterfacesAdded'");
477
478 auto m = set(trigger5.str);
479 m.append("xyz.openbmc_project.Example.Iface2");
480 m.append("ExampleProperty2");
481 m.append(sdbusplus::message::variant<std::string>("abc123"));
482 b.call(m);
483 {
484 auto sig{queue.pop()};
485 assert(sig);
486 sig.read(signalPath);
487 assert(createMe1 == signalPath.str);
488 sig.read(signalObject);
489 }
490 {
491 auto sig{queue.pop()};
492 assert(sig);
493 sig.read(signalPath);
494 assert(createMe2 == signalPath.str);
495 sig.read(signalObject);
496 }
497
498 auto moreSignals{queue.pop()};
499 assert(!moreSignals);
500 }
501 }
Brad Bishop49aefb32016-10-19 11:54:14 -0400502}
503
504int main()
505{
Brad Bishop65247582017-01-15 19:48:41 -0500506 phosphor::inventory::manager::Manager mgr(
507 sdbusplus::bus::new_default(),
Brad Bishop8f868502017-01-23 13:13:58 -0500508 MGR_SERVICE, MGR_ROOT, MGR_INTERFACE);
509 ExampleService d;
Brad Bishop49aefb32016-10-19 11:54:14 -0400510
Brad Bishop8f868502017-01-23 13:13:58 -0500511 auto f1 = [](auto mgr)
Brad Bishop49aefb32016-10-19 11:54:14 -0400512 {
Brad Bishop21621432017-01-13 16:35:53 -0500513 mgr->run();
514 };
Brad Bishop8f868502017-01-23 13:13:58 -0500515 auto f2 = [](auto d)
516 {
517 d->run();
518 };
Brad Bishop49aefb32016-10-19 11:54:14 -0400519
Brad Bishop8f868502017-01-23 13:13:58 -0500520 auto t1 = std::thread(f1, &mgr);
521 auto t2 = std::thread(f2, &d);
522
523 runTests();
524
525 mgr.shutdown();
526 d.shutdown = true;
527
528 // Wait for server threads to exit.
529 t1.join();
530 t2.join();
531 std::cout << "Success! Waiting for threads to exit..." << std::endl;
Brad Bishop49aefb32016-10-19 11:54:14 -0400532
533 return 0;
534}
535
536// vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4