summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArtem Dyomin <artem.dyomin@qt.io>2022-12-19 14:18:22 +0100
committerArtem Dyomin <artem.dyomin@qt.io>2022-12-22 09:58:24 +0100
commit15190f347bac9b2e4a8c855d67e864051eb514d2 (patch)
tree5e7c333d02742f35921b6a92d166e3a585e994f8
parent45f01e24293aa5a7ba625530dc8b573b82544eea (diff)
Implement utility for checking and dumping audio config on macOS
What is the utility able to do: - Enquires a vast set of audio devices configurations via AudioObjectGetPropertyData. - dumps results. - check the configuration many times in order to check if it's stable. Why do we need it: We have bugs with QtMM on macOS with audio input/output that are not reproducible with development environments. Pretty often such bugs have smth to do with specific audio devices on the bug reporter's side, and it's unclear what AudioObjectGetPropertyData returns in these cases. When I was investigating previous bugs, I attached such a code to the ticket so that bug reporters could provide more info. In light of the above, it's reasonable to have such a utility in the qt repo. Having it now in a single file could simplify its compilation and run by customers (just one command in README). Pick-to: 6.5 6.4 Task-number: QTBUG-108176 Change-Id: I3560353c78a0852a426f25b3487ab0a93e39c03f Reviewed-by: Piotr Srebrny <piotr.srebrny@qt.io> Reviewed-by: Lars Knoll <lars@knoll.priv.no>
-rw-r--r--util/macos_test_audio_config/CMakeLists.txt13
-rw-r--r--util/macos_test_audio_config/README25
-rwxr-xr-xutil/macos_test_audio_config/compile_and_run.sh19
-rw-r--r--util/macos_test_audio_config/main.cpp498
4 files changed, 555 insertions, 0 deletions
diff --git a/util/macos_test_audio_config/CMakeLists.txt b/util/macos_test_audio_config/CMakeLists.txt
new file mode 100644
index 000000000..d99987865
--- /dev/null
+++ b/util/macos_test_audio_config/CMakeLists.txt
@@ -0,0 +1,13 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.5)
+
+project(test_audio_config LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+add_executable(test_audio_config main.cpp)
+
+target_link_libraries(test_audio_config "-framework Coreaudio" "-framework CoreFoundation")
diff --git a/util/macos_test_audio_config/README b/util/macos_test_audio_config/README
new file mode 100644
index 000000000..560d84100
--- /dev/null
+++ b/util/macos_test_audio_config/README
@@ -0,0 +1,25 @@
+This utility tests stability of audio configuration on macOS, and prints it.
+It reads audio config via AudioObjectGetPropertyData many times, checks if it changes, and dumps results.
+It might be useful to check the configuration if specific audio devices are installed on your PC
+and Qt multimedia handles them incorrectly or writes warnings to the console.
+It's a good idea to attach a text file with the utility's output when creating a bug for audio on macOS.
+In order to test an unstable config, set some sufficient testing time and just attach/detach a device.
+
+
+Build:
+
+mkdir build
+cd build
+cmake ..
+make
+
+
+Run:
+
+./test_audio_config
+Running without parameters performs some default testing. Use --help to see customization details.
+
+
+Lazy build, run, and dump the result to file test_audio_config_res.txt:
+
+./compile_and_run.sh
diff --git a/util/macos_test_audio_config/compile_and_run.sh b/util/macos_test_audio_config/compile_and_run.sh
new file mode 100755
index 000000000..6cd477769
--- /dev/null
+++ b/util/macos_test_audio_config/compile_and_run.sh
@@ -0,0 +1,19 @@
+#! /bin/sh
+# Copyright (C) 2019 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+BUILD_DIR="__build__"
+OUTPUT_FILE="test_audio_config_res.txt"
+
+mkdir $BUILD_DIR
+cd $BUILD_DIR
+cmake ..
+make -j
+cd ..
+
+rm -f $OUTPUT_FILE
+./$BUILD_DIR/test_audio_config "$@" >> $OUTPUT_FILE
+cat $OUTPUT_FILE
+echo "\nThe result has been written to file $OUTPUT_FILE\n"
+
+rm -rf $BUILD_DIR
diff --git a/util/macos_test_audio_config/main.cpp b/util/macos_test_audio_config/main.cpp
new file mode 100644
index 000000000..43892b4a7
--- /dev/null
+++ b/util/macos_test_audio_config/main.cpp
@@ -0,0 +1,498 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include <iostream>
+#include <vector>
+#include <optional>
+#include <unordered_map>
+#include <sstream>
+#include <thread>
+#include <chrono>
+#include <algorithm>
+
+#include <CoreAudio/AudioHardware.h>
+
+static constexpr std::uint32_t ElementMaster =
+ kAudioObjectPropertyElementMaster; // TODO: use kAudioObjectPropertyElementMain for macOS
+ // versions >= 12.0
+static constexpr std::uint32_t LogOffsetSize = 2;
+
+static constexpr std::chrono::milliseconds DefaultTestingTime(1000);
+static constexpr std::chrono::milliseconds DefaultTestingInterval(0);
+
+struct Flags
+{
+ std::uint32_t value = 0;
+};
+
+struct Code
+{
+ std::uint32_t code = 0;
+};
+
+template<typename T>
+struct Span
+{
+ const T *data = nullptr;
+ size_t size = 0;
+};
+
+struct LogOffset
+{
+ std::uint32_t value = 0;
+
+ LogOffset operator+(std::uint32_t v) const { return LogOffset{ value + v }; }
+};
+
+namespace std {
+ostream &operator<<(ostream &os, const Flags &flags)
+{
+ os << '[';
+ std::uint32_t index = 0;
+ bool first = true;
+ for (auto value = flags.value; value; value >>= 1, ++index) {
+ if ((value & 1) != 0) {
+ if (!first)
+ os << ',';
+ os << index;
+ first = false;
+ }
+ }
+ os << ']';
+
+ return os;
+}
+
+ostream &operator<<(ostream &os, const Code &code)
+{
+ os << '{' << code.code << '|';
+
+ const char *desc = reinterpret_cast<const char *>(&code.code);
+
+ std::copy_n(std::make_reverse_iterator(desc + sizeof(code.code)), sizeof(code.code),
+ std::ostream_iterator<char>(os));
+
+ os << '}';
+
+ return os;
+}
+
+ostream &operator<<(ostream &os, const LogOffset &offset)
+{
+ std::fill_n(std::ostream_iterator<char>(os), offset.value * LogOffsetSize, ' ');
+
+ return os;
+}
+
+template<typename T>
+ostream &operator<<(ostream &os, const Span<T> &span)
+{
+ os << '[';
+ if (span.size)
+ os << span.data[0];
+
+ for (size_t i = 1; i < span.size; ++i)
+ os << ',' << span.data[i];
+
+ os << ']';
+
+ return os;
+}
+
+ostream &operator<<(ostream &os, const CFStringRef &str)
+{
+ const auto originalBuffer = CFStringGetCStringPtr(str, kCFStringEncodingUTF8);
+ if (originalBuffer) {
+ os << originalBuffer;
+ } else {
+ const auto lengthInUtf16 = CFStringGetLength(str);
+ const auto maxLengthInUtf8 =
+ CFStringGetMaximumSizeForEncoding(lengthInUtf16, kCFStringEncodingUTF8) + 1;
+ std::vector<char> localBuffer(maxLengthInUtf8);
+
+ if (CFStringGetCString(str, localBuffer.data(), maxLengthInUtf8, maxLengthInUtf8))
+ os << localBuffer.data();
+ else
+ os << "{empty}";
+ }
+
+ return os;
+}
+
+} // namespace std
+
+template<typename T = char>
+static std::optional<std::vector<T>>
+getAudioData(std::ostream &os, const LogOffset &offset, AudioObjectID inObjectID,
+ const AudioObjectPropertyAddress &inAddress, size_t minDataSize = 0)
+{
+ static_assert(std::is_trivial_v<T>, "Trivial type is expected");
+
+ UInt32 propSize = 0;
+ const auto res = AudioObjectGetPropertyDataSize(inObjectID, &inAddress, 0, nullptr, &propSize);
+
+ if (res == noErr) {
+ if (propSize / sizeof(T) < minDataSize) {
+ os << offset << "Data size is too low: actual " << propSize << "B vs expected " << minDataSize * sizeof(T) << "B\n";
+ return {};
+ }
+
+ std::vector<T> data(propSize / sizeof(T));
+
+ if (data.size() * sizeof(T) != propSize)
+ os << offset << "Probably, wrong data size: " << propSize << ", Element size: " << sizeof(T) << '\n';
+
+ const auto res = AudioObjectGetPropertyData(inObjectID, &inAddress, 0, nullptr, &propSize,
+ data.data());
+
+ if (res == noErr)
+ return { std::move(data) };
+ else
+ os << offset << "AudioObjectGetPropertyData failed, Err: "
+ << Code{ static_cast<std::uint32_t>(res) } << '\n';
+ } else {
+ os << offset << "AudioObjectGetPropertyDataSize failed, Err: "
+ << Code{ static_cast<std::uint32_t>(res) } << '\n';
+ }
+
+ return {};
+}
+
+template<typename T = char>
+static std::optional<T> getAudioObject(std::ostream &os, const LogOffset &offset,
+ AudioObjectID inObjectID,
+ const AudioObjectPropertyAddress &inAddress)
+{
+ if (auto data = getAudioData<T>(os, offset, inObjectID, inAddress, 1)) {
+ if (data->size() > 1)
+ os << offset << "Warn: unexpected data size: " << data->size() << '\n';
+
+ return { data->front() };
+ }
+
+ return {};
+}
+
+static void dumpFormats(std::ostream &os, const LogOffset &offset, AudioDeviceID id,
+ std::uint32_t scope)
+{
+ os << offset << "Formats:\n";
+
+ const AudioObjectPropertyAddress audioDevicePropertyStreamsAddress{ kAudioDevicePropertyStreams,
+ scope, ElementMaster };
+
+ if (auto streamIDs = getAudioData<AudioStreamID>(os, offset + 1, id,
+ audioDevicePropertyStreamsAddress)) {
+ for (auto &streamID : *streamIDs) {
+ os << offset + 1 << "Stream id: " << streamID << '\n';
+
+ auto dumpCurrentFormat = [&](std::uint32_t selector) {
+ const AudioObjectPropertyAddress propertyAddress{ selector, scope, ElementMaster };
+
+ if (auto format = getAudioObject<AudioStreamBasicDescription>(
+ os, offset + 3, streamID, propertyAddress)) {
+
+ os << offset + 3 << "mFormatID: " << format->mFormatID << '\n';
+ os << offset + 3 << "mSampleRate: " << format->mSampleRate << '\n';
+ os << offset + 3 << "mFormatFlags: " << Flags{ format->mFormatFlags } << '\n';
+ }
+ };
+
+ auto dumpAvailableFormats = [&](std::uint32_t selector) {
+ const AudioObjectPropertyAddress propertyAddress{ selector, scope, ElementMaster };
+
+ if (auto descriptions = getAudioData<AudioStreamRangedDescription>(
+ os, offset + 3, streamID, propertyAddress)) {
+
+ size_t index = 0;
+ for (const AudioStreamRangedDescription &desc : *descriptions) {
+
+ const auto &format = desc.mFormat;
+ os << offset + 3 << "mFormatID: " << format.mFormatID << " (Index " << index
+ << ")\n";
+
+ os << offset + 4 << "mSampleRateRange: [" << desc.mSampleRateRange.mMinimum
+ << "; " << desc.mSampleRateRange.mMaximum << "]\n"
+ << offset + 4 << "mSampleRate: " << format.mSampleRate << '\n'
+ << offset + 4 << "mFormatFlags: " << Flags{ format.mFormatFlags } << '\n'
+ << offset + 4 << "mBytesPerPacket: " << format.mBytesPerPacket << '\n'
+ << offset + 4 << "mFramesPerPacket: " << format.mFramesPerPacket << '\n'
+ << offset + 4 << "mBytesPerFrame: " << format.mBytesPerFrame << '\n'
+ << offset + 4 << "mChannelsPerFrame: " << format.mChannelsPerFrame << '\n'
+ << offset + 4 << "mBitsPerChannel: " << format.mBitsPerChannel << '\n';
+
+ ++index;
+ }
+ }
+ };
+
+ os << offset + 2 << "Preffered physical format "
+ << Code{ kAudioStreamPropertyPhysicalFormat } << ":\n";
+ dumpCurrentFormat(kAudioStreamPropertyPhysicalFormat);
+
+ os << offset + 2 << "Available physical formats "
+ << Code{ kAudioStreamPropertyAvailablePhysicalFormats } << ":\n";
+ dumpAvailableFormats(kAudioStreamPropertyAvailablePhysicalFormats);
+
+ os << offset + 2 << "Preffered virtual format "
+ << Code{ kAudioStreamPropertyVirtualFormat } << ":\n";
+ dumpCurrentFormat(kAudioStreamPropertyVirtualFormat);
+
+ os << offset + 2 << "Available virtual formats "
+ << Code{ kAudioStreamPropertyAvailableVirtualFormats } << ":\n";
+ dumpAvailableFormats(kAudioStreamPropertyAvailableVirtualFormats);
+ }
+ }
+}
+
+static void dumpChannelsLayout(std::ostream &os, const LogOffset &offset, AudioDeviceID id,
+ std::uint32_t scope)
+{
+ os << offset << "Channels Layout " << Code{ kAudioDevicePropertyPreferredChannelLayout }
+ << ":\n";
+
+ const AudioObjectPropertyAddress audioDeviceChannelLayoutPropertyAddress{
+ kAudioDevicePropertyPreferredChannelLayout, scope, ElementMaster
+ };
+
+ if (auto data = getAudioData(os, offset + 1, id, audioDeviceChannelLayoutPropertyAddress,
+ sizeof(AudioChannelLayout))) {
+ const AudioChannelLayout &layout = *reinterpret_cast<AudioChannelLayout *>(data->data());
+
+ os << offset + 1 << "mChannelLayoutTag: " << layout.mChannelLayoutTag << "\n"
+ << offset + 1 << "mChannelBitmap: " << Flags{ layout.mChannelBitmap } << "\n"
+ << offset + 1 << "mNumberChannelDescriptions: " << layout.mNumberChannelDescriptions << "\n"
+ << offset + 1 << "ChannelDescriptions:\n";
+
+ for (UInt32 i = 0; i < layout.mNumberChannelDescriptions; ++i) {
+ const auto &desc = layout.mChannelDescriptions[i];
+ os << offset + 2 << "Channel " << i << ":\n";
+
+ os << offset + 3 << "mChannelLabel: " << desc.mChannelLabel;
+ if (desc.mChannelLabel == 0xFFFFFFFF)
+ os << " (unknown)";
+ os << '\n';
+
+ os << offset + 3 << "mChannelFlags: " << Flags{ desc.mChannelFlags } << '\n'
+ << offset + 3 << "mCoordinates: " << Span<Float32>{ desc.mCoordinates, 3 } << '\n';
+ }
+ }
+}
+
+static void dumpBasicDescription(std::ostream &os, const LogOffset &offset, AudioDeviceID id,
+ std::uint32_t scope)
+{
+ os << offset << "Basic Description " << Code{ kAudioDevicePropertyStreamFormat } << ":\n";
+
+ const AudioObjectPropertyAddress audioDeviceStreamFormatPropertyAddress{
+ kAudioDevicePropertyStreamFormat, scope, ElementMaster
+ };
+
+ if (auto basicDescr = getAudioObject<AudioStreamBasicDescription>(
+ os, offset + 1, id, audioDeviceStreamFormatPropertyAddress)) {
+ os << offset + 1 << "mSampleRate: " << basicDescr->mSampleRate << "\n"
+ << offset + 1 << "mFormatID: " << basicDescr->mFormatID << "\n"
+ << offset + 1 << "mFormatFlags: " << Flags{ basicDescr->mFormatFlags } << "\n"
+ << offset + 1 << "mBytesPerPacket: " << basicDescr->mBytesPerPacket << "\n"
+ << offset + 1 << "mFramesPerPacket: " << basicDescr->mFramesPerPacket << "\n"
+ << offset + 1 << "mBytesPerFrame: " << basicDescr->mBytesPerFrame << "\n"
+ << offset + 1 << "mChannelsPerFrame: " << basicDescr->mChannelsPerFrame << "\n"
+ << offset + 1 << "mBitsPerChannel: " << basicDescr->mBitsPerChannel << "\n";
+ }
+}
+
+static void dumpGeneralDeviceInfo(std::ostream &os, const LogOffset &offset, AudioDeviceID id,
+ std::uint32_t scope)
+{
+ os << offset << "General Device Info:\n";
+
+ auto dumpString = [&](const char *name, std::uint32_t selector) {
+ os << offset + 1 << name << " " << Code{ selector } << ":\n";
+ const AudioObjectPropertyAddress propertyAddress{ selector, scope, ElementMaster };
+
+ if (auto str = getAudioObject<CFStringRef>(os, offset + 2, id, propertyAddress)) {
+ os << offset + 2 << *str << '\n';
+ CFRelease(*str);
+ }
+ };
+
+ auto dumpBool = [&](const char *name, std::uint32_t selector) {
+ os << offset + 1 << name << ' ' << Code{ selector } << ":\n";
+ const AudioObjectPropertyAddress propertyAddress{ selector, scope, ElementMaster };
+
+ if (auto value = getAudioObject<UInt32>(os, offset + 2, id, propertyAddress))
+ os << offset + 2 << std::boolalpha << static_cast<bool>(*value) << '\n';
+ };
+
+ dumpString("Name", kAudioObjectPropertyName);
+ dumpString("Manufacturer", kAudioObjectPropertyManufacturer);
+ dumpString("Element name", kAudioObjectPropertyElementName);
+ dumpString("Model name", kAudioObjectPropertyModelName);
+
+ dumpBool("Device is alive", kAudioDevicePropertyDeviceIsAlive);
+ dumpBool("Device is running", kAudioDevicePropertyDeviceIsRunning);
+ dumpBool("Can be default device", kAudioDevicePropertyDeviceCanBeDefaultDevice);
+ dumpBool("Can be default system device", kAudioDevicePropertyDeviceCanBeDefaultSystemDevice);
+ dumpBool("Device property is hidden", kAudioDevicePropertyIsHidden);
+
+ {
+ const AudioObjectPropertyAddress propertyAddress{
+ kAudioDevicePropertyPreferredChannelsForStereo, scope, ElementMaster
+ };
+
+ os << offset + 1 << "Preffered channels for stereo "
+ << Code{ kAudioDevicePropertyPreferredChannelsForStereo } << ":\n";
+ if (auto data = getAudioData<UInt32>(os, offset + 2, id, propertyAddress, 2))
+ os << offset + 2 << Span<UInt32>{ data->data(), 2 } << '\n';
+ }
+}
+
+static void dumpAvailableAudioDevices(std::ostream &os, const LogOffset &offset,
+ std::uint32_t scope)
+{
+ os << offset << "Dump devices " << Code{ kAudioHardwarePropertyDevices }
+ << ", scope: " << Code{ scope } << "\n";
+ const AudioObjectPropertyAddress audioDevicesPropertyAddress{ kAudioHardwarePropertyDevices,
+ scope, ElementMaster };
+
+ if (auto devices = getAudioData<AudioDeviceID>(os, offset + 1, kAudioObjectSystemObject,
+ audioDevicesPropertyAddress)) {
+ size_t index = 0;
+ for (auto id : *devices) {
+ os << offset + 2 << "ID: " << id << " (Index " << index << ")\n";
+
+ dumpGeneralDeviceInfo(os, offset + 3, id, scope);
+
+ dumpBasicDescription(os, offset + 3, id, scope);
+
+ dumpChannelsLayout(os, offset + 3, id, scope);
+
+ dumpFormats(os, offset + 3, id, scope);
+
+ ++index;
+ }
+ }
+}
+
+static void dumpAvailableAudioDevices(std::ostream &os, const LogOffset &offset)
+{
+ for (auto scope : { kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput }) {
+ dumpAvailableAudioDevices(os, offset, scope);
+ os << offset << "\n";
+ }
+}
+
+static void testStability(const std::chrono::milliseconds &time,
+ const std::chrono::milliseconds &interval)
+{
+ std::cout << "Start testing audio config...\n" << std::endl;
+
+ std::unordered_map<std::string, std::uint32_t> results;
+ std::uint32_t counter = 0;
+
+ const auto end = std::chrono::system_clock::now() + time;
+
+ while (true) {
+ std::ostringstream stream;
+
+ dumpAvailableAudioDevices(stream, {});
+
+ ++counter;
+ ++results[stream.str()];
+
+ if (std::chrono::system_clock::now() + interval >= end)
+ break;
+
+ std::this_thread::sleep_for(interval);
+ }
+
+ std::cout << "Audio config has been tested for " << time.count() << "ms\n" <<
+ "Set --time %your time% in order to change the testing time.\n" <<
+ "The config has been taken " << counter << " times\n" <<
+ "------------------------------------------------------------\n";
+
+ std::cout << std::endl;
+
+ using Result = decltype(results)::value_type;
+ std::vector<std::reference_wrapper<Result>> resultRefs(results.begin(), results.end());
+ std::sort(resultRefs.begin(), resultRefs.end(),
+ [](const Result &a, const Result &b) { return a.second < b.second; });
+
+ for (size_t i = 0; i < resultRefs.size(); ++i) {
+ const Result &res = resultRefs[i];
+ std::cout << "Result N" << i + 1 << "; Occurs: " << res.second << '/' << counter << '\n'
+ << res.first;
+ }
+
+ if (results.size() == 1)
+ std::cout << "The config seems to be stable (" << counter << " times)\n";
+ else
+ std::cout << "The config is unstable: " << results.size() << " different results\n";
+
+ std::cout << "\nTesting done!" << std::endl;
+}
+
+static void printHelp()
+{
+ // clang-format off
+ std::cout << "This utility tests stability of audio configuration on macOS, and prints it.\n"
+ "It reads audio config via AudioObjectGetPropertyData many times, checks if it changes and dumps results.\n"
+ "It might be useful to check the configuration if specific audio devices are installed on your PC\n"
+ "and Qt multimedia handles them incorrectly or writes warnings to the console.\n"
+ "It's a good idea to attach a text file with the utility's output when creating a bug for audio on macOS.\n"
+ "In order to test an unstable config, set some sufficient testing time and just attach/detach a device.\n"
+ "Options:\n"
+ " --time Common time of testing in ms. Defaults to " << DefaultTestingTime.count() << ".\n"
+ " Set --time 0 for simple duming of the audio config.\n"
+ " --interval Interval in ms between reading of audio config. Defaults to " << DefaultTestingInterval.count() << ".\n"
+ " --help Show help.";
+ // clang-format on
+
+ std::cout << std::endl;
+}
+
+int main(int argc, char *argv[])
+{
+ std::chrono::milliseconds testingTime = DefaultTestingTime;
+ std::chrono::milliseconds testingInterval = DefaultTestingInterval;
+
+ for (int i = 1; i < argc; ++i) {
+ auto getIntValue = [&]() -> std::optional<int> {
+ if (i + 1 < argc) {
+ char *end = argv[i + 1];
+ const auto val = strtol(argv[i + 1], &end, 10);
+ ++i;
+ if (*end == '\0')
+ return { val };
+ else
+ std::cout << "Cannot read value " << argv[i + 1] << std::endl;
+ } else {
+ std::cout << "Cannot read value" << std::endl;
+ }
+
+ return {};
+ };
+
+ if (strcmp(argv[i], "--time") == 0) {
+ if (auto time = getIntValue())
+ testingTime = std::chrono::milliseconds(*time);
+ else
+ return 1;
+ } else if (strcmp(argv[i], "--interval") == 0) {
+ if (auto interval = getIntValue())
+ testingInterval = std::chrono::milliseconds(*interval);
+ else
+ return 1;
+ } else if (strcmp(argv[i], "--help") == 0) {
+ printHelp();
+ return 0;
+ } else {
+ std::cout << "Wrong option " << argv[i] << std::endl;
+ return 1;
+ }
+ }
+
+ testStability(testingTime, testingInterval);
+
+ return 0;
+}