LCOV - code coverage report
Current view: top level - muse/framework/cloud/audiocom - audiocomservice.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 280 0
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2025 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : 
      23              : #include "audiocomservice.h"
      24              : 
      25              : #include <QJsonDocument>
      26              : #include <QJsonObject>
      27              : #include <QBuffer>
      28              : #include <QHttpMultiPart>
      29              : 
      30              : #include "async/async.h"
      31              : #include "containers.h"
      32              : 
      33              : #include "clouderrors.h"
      34              : 
      35              : #include "defer.h"
      36              : #include "log.h"
      37              : 
      38              : using namespace muse;
      39              : using namespace muse::cloud;
      40              : using namespace muse::network;
      41              : using namespace muse::async;
      42              : 
      43              : static const QString AUDIOCOM_CLOUD_TITLE("Audio.com");
      44              : static const QString AUDIOCOM_CLOUD_URL("https://audio.com");
      45              : static const QString AUDIOCOM_API_ROOT_URL("https://api.audio.com");
      46              : static const QUrl AUDIOCOM_USER_INFO_API_URL(AUDIOCOM_API_ROOT_URL + "/me");
      47              : 
      48              : static const QUrl AUDIOCOM_UPLOAD_AUDIO_API_URL(AUDIOCOM_API_ROOT_URL + "/audio");
      49              : 
      50              : static const QString AUDIOCOM_LOGO_URL(AUDIOCOM_CLOUD_URL + "/img/mu-app-logo.svg");
      51              : 
      52            0 : static QString audioMime(const QString& audioFormat)
      53              : {
      54            0 :     if (audioFormat == "mp3") {
      55            0 :         return "audio/mpeg";
      56              :     }
      57              : 
      58            0 :     return "audio/x-wav";
      59              : }
      60              : 
      61            0 : static RetVal<AccountInfo> parseAudioComAccountInfo(const QByteArray& data)
      62              : {
      63            0 :     QJsonParseError err;
      64            0 :     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
      65            0 :     if (err.error != QJsonParseError::NoError || !doc.isObject()) {
      66            0 :         return muse::make_ret(Ret::Code::InternalError, err.errorString().toStdString());
      67              :     }
      68              : 
      69            0 :     QJsonObject user = doc.object();
      70            0 :     QString profileUrl = AUDIOCOM_CLOUD_URL + "/" + user.value("username").toString();
      71              : 
      72            0 :     AccountInfo info;
      73            0 :     info.id = user.value("id").toString();
      74            0 :     info.userName = user.value("profile").toObject().value("name").toString();
      75            0 :     info.profileUrl = QUrl(profileUrl);
      76            0 :     info.collectionUrl = info.profileUrl;
      77            0 :     info.avatarUrl = QUrl(user.value("avatar").toString());
      78              : 
      79            0 :     return RetVal<AccountInfo>::make_ok(info);
      80            0 : }
      81              : 
      82            0 : AudioComService::AudioComService(const modularity::ContextPtr& iocCtx, QObject* parent)
      83            0 :     : AbstractCloudService(iocCtx, parent)
      84              : {
      85            0 : }
      86              : 
      87            0 : IAuthorizationServicePtr AudioComService::authorization()
      88              : {
      89            0 :     return shared_from_this();
      90              : }
      91              : 
      92            0 : CloudInfo AudioComService::cloudInfo() const
      93              : {
      94              :     return {
      95              :         AUDIO_COM_CLOUD_CODE,
      96              :         AUDIOCOM_CLOUD_TITLE,
      97              :         AUDIOCOM_CLOUD_URL,
      98              :         AUDIOCOM_LOGO_URL,
      99              :         logoColor()
     100            0 :     };
     101            0 : }
     102              : 
     103            0 : QUrl AudioComService::projectManagerUrl() const
     104              : {
     105            0 :     return accountInfo().profileUrl.toString() + "/projects";
     106              : }
     107              : 
     108            0 : AbstractCloudService::ServerConfig AudioComService::serverConfig() const
     109              : {
     110            0 :     ServerConfig serverConfig;
     111            0 :     serverConfig.serverCode = AUDIO_COM_CLOUD_CODE;
     112            0 :     serverConfig.serverUrl = AUDIOCOM_CLOUD_URL;
     113              : 
     114            0 :     serverConfig.serverAvailabilityUrl = AUDIOCOM_API_ROOT_URL + "/system/healthcheck";
     115              : 
     116            0 :     serverConfig.authorizationUrl = AUDIOCOM_CLOUD_URL + "/auth/sign-in";
     117            0 :     serverConfig.signUpUrl = AUDIOCOM_CLOUD_URL + "/auth/sign-up";
     118            0 :     serverConfig.signInSuccessUrl = AUDIOCOM_CLOUD_URL + "/my-audio?muAuthSuccess=true";
     119              : 
     120            0 :     serverConfig.accessTokenUrl = AUDIOCOM_API_ROOT_URL + "/auth/token";
     121            0 :     serverConfig.refreshApiUrl = AUDIOCOM_API_ROOT_URL + "/auth/token";
     122              : 
     123            0 :     serverConfig.headers = headers();
     124              : 
     125            0 :     static const QString CLIENT_ID = "1760627154191472";
     126            0 :     static const QString CLIENT_SECRET = "0qLugLJwBXsm8e-fOd3WgFx6TOTQkw74";
     127              : 
     128              :     serverConfig.authorizationParameters = {
     129              :         { "client_id", CLIENT_ID },
     130              :         { "client_secret", CLIENT_SECRET },
     131              :         { "grant_type", "authorization_code" },
     132              :         { "scope", "all" }
     133            0 :     };
     134              : 
     135              :     serverConfig.refreshParameters = {
     136              :         { "client_id", CLIENT_ID },
     137              :         { "client_secret", CLIENT_SECRET },
     138              :         { "grant_type", "refresh_token" }
     139            0 :     };
     140              : 
     141            0 :     return serverConfig;
     142            0 : }
     143              : 
     144            0 : RequestHeaders AudioComService::headers(const QString& token) const
     145              : {
     146            0 :     RequestHeaders headers = defaultHeaders();
     147            0 :     headers.rawHeaders["Accept"] = "application/json";
     148            0 :     headers.rawHeaders["Content-Type"] = "application/json";
     149            0 :     headers.rawHeaders["Authorization"] = QString("Bearer " + (!token.isEmpty() ? token : accessToken())).toUtf8();
     150              : 
     151            0 :     return headers;
     152            0 : }
     153              : 
     154            0 : Promise<Ret> AudioComService::downloadAccountInfo()
     155              : {
     156            0 :     TRACEFUNC;
     157              : 
     158            0 :     return make_promise<Ret>([this](auto resolve, auto) {
     159            0 :         auto receivedData = std::make_shared<QBuffer>();
     160            0 :         RetVal<Progress> progress = m_networkManager->get(AUDIOCOM_USER_INFO_API_URL, receivedData, headers());
     161            0 :         if (!progress.ret) {
     162            0 :             return resolve(progress.ret);
     163              :         }
     164              : 
     165            0 :         progress.val.finished().onReceive(this, [this, receivedData, resolve](const ProgressResult& res) {
     166            0 :             if (!res.ret) {
     167            0 :                 printServerReply(*receivedData);
     168            0 :                 (void)resolve(res.ret);
     169            0 :                 return;
     170              :             }
     171              : 
     172            0 :             RetVal<AccountInfo> info = parseAudioComAccountInfo(receivedData->data());
     173            0 :             if (!info.ret) {
     174            0 :                 (void)resolve(info.ret);
     175            0 :                 return;
     176              :             }
     177              : 
     178            0 :             if (info.val.isValid()) {
     179            0 :                 setAccountInfo(info.val);
     180              :             } else {
     181            0 :                 setAccountInfo(AccountInfo());
     182              :             }
     183              : 
     184            0 :             (void)resolve(make_ok());
     185            0 :         });
     186              : 
     187            0 :         return Promise<Ret>::dummy_result();
     188            0 :     });
     189            0 : }
     190              : 
     191            0 : Promise<Ret> AudioComService::updateTokens()
     192              : {
     193            0 :     TRACEFUNC;
     194              : 
     195            0 :     return make_promise<Ret>([this](auto resolve, auto) {
     196            0 :         ServerConfig serverConfig = this->serverConfig();
     197              : 
     198            0 :         QJsonObject json;
     199            0 :         json["refresh_token"] = refreshToken();
     200              : 
     201            0 :         for (const QString& key : serverConfig.authorizationParameters.keys()) {
     202            0 :             json.insert(key, serverConfig.refreshParameters.value(key).toString());
     203              :         }
     204              : 
     205            0 :         QByteArray jsonData = QString::fromStdString(QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact).toStdString()).toUtf8();
     206            0 :         auto outgoingData = std::make_shared<QBuffer>();
     207            0 :         outgoingData->setData(jsonData);
     208            0 :         auto receivedData = std::make_shared<QBuffer>();
     209              : 
     210            0 :         RetVal<Progress> progress = m_networkManager->post(serverConfig.refreshApiUrl, outgoingData, receivedData, headers());
     211            0 :         if (!progress.ret) {
     212            0 :             return resolve(progress.ret);
     213              :         }
     214              : 
     215            0 :         progress.val.finished().onReceive(this, [this, receivedData, resolve](const ProgressResult& res) {
     216            0 :             if (!res.ret) {
     217            0 :                 printServerReply(*receivedData);
     218            0 :                 (void)resolve(res.ret);
     219            0 :                 return;
     220              :             }
     221              : 
     222            0 :             QJsonParseError err;
     223            0 :             QJsonDocument doc = QJsonDocument::fromJson(receivedData->data(), &err);
     224            0 :             if (err.error != QJsonParseError::NoError || !doc.isObject()) {
     225            0 :                 (void)resolve(Ret((int)Err::UnknownError, err.errorString().toStdString()));
     226            0 :                 return;
     227              :             }
     228              : 
     229            0 :             QJsonObject tokens = doc.object();
     230            0 :             setAccessToken(tokens.value(ACCESS_TOKEN_KEY).toString());
     231            0 :             setRefreshToken(tokens.value(REFRESH_TOKEN_KEY).toString());
     232            0 :             (void)resolve(make_ok());
     233            0 :         });
     234              : 
     235            0 :         return Promise<Ret>::dummy_result();
     236            0 :     });
     237            0 : }
     238              : 
     239            0 : ProgressPtr AudioComService::uploadAudio(DevicePtr audioData, const QString& audioFormat, const QString& title, const QUrl& existingUrl,
     240              :                                          Visibility visibility, bool replaceExisting)
     241              : {
     242            0 :     ProgressPtr progress = std::make_shared<Progress>();
     243            0 :     progress->start();
     244              : 
     245            0 :     auto finishProgress = [this, progress](const Ret& ret) {
     246            0 :         ProgressResult result(ret);
     247              : 
     248            0 :         DEFER {
     249            0 :             m_currentUploadingAudioSlug.clear();
     250            0 :             m_currentUploadingAudioId.clear();
     251            0 :             m_currentUploadingAudioInfo = {};
     252            0 :             progress->finish(result);
     253            0 :         };
     254              : 
     255            0 :         if (!ret) {
     256            0 :             return;
     257              :         }
     258              : 
     259            0 :         ValMap audioMap;
     260            0 :         audioMap["editUrl"] = Val(QString("%2/audio/%3/edit").arg(
     261            0 :                                       accountInfo().collectionUrl.toString(), m_currentUploadingAudioSlug));
     262            0 :         audioMap["url"] = Val(AUDIOCOM_CLOUD_URL + "/audio/" + m_currentUploadingAudioId);
     263            0 :         result.val = Val(audioMap);
     264            0 :     };
     265              : 
     266            0 :     if (replaceExisting) {
     267            0 :         executeAsyncRequest([this, audioData, audioFormat, title, existingUrl, visibility, progress]() {
     268            0 :             return replaceExistingAudio(audioData, audioFormat, title, existingUrl, visibility, progress);
     269            0 :         }).onResolve(this, [finishProgress](const Ret& ret) {
     270            0 :             finishProgress(ret);
     271            0 :         });
     272              :     } else {
     273            0 :         executeAsyncRequest([this, audioData, audioFormat, title, existingUrl, visibility, progress]() {
     274            0 :             return uploadNewAudio(audioData, audioFormat, title, existingUrl, visibility, progress);
     275            0 :         }).onResolve(this, [finishProgress](const Ret& ret) {
     276            0 :             finishProgress(ret);
     277            0 :         });
     278              :     }
     279              : 
     280            0 :     return progress;
     281            0 : }
     282              : 
     283            0 : Promise<Ret> AudioComService::uploadNewAudio(DevicePtr audioData, const QString& audioFormat, const QString& title, const QUrl& url,
     284              :                                              Visibility visibility, ProgressPtr progress)
     285              : {
     286            0 :     std::weak_ptr<QIODevice> audioDataWeakPtr = audioData; // prevents memory leak
     287              : 
     288              :     // Create audio info -> upload audio
     289            0 :     return doCreateAudio(title, audioData->size(), audioFormat, url, visibility, false /*replaceExisting*/)
     290            0 :            .then<Ret>(this, [this, audioDataWeakPtr, audioFormat, progress](const Ret& ret, auto resolve) {
     291            0 :         DevicePtr audioData = audioDataWeakPtr.lock();
     292            0 :         if (!ret || !audioData) {
     293            0 :             return resolve(ret);
     294              :         }
     295              : 
     296            0 :         doUploadAudio(audioData, audioFormat, progress).onResolve(this, [resolve](const Ret& ret) {
     297            0 :             (void)resolve(ret);
     298              :         });
     299              : 
     300            0 :         return Promise<Ret>::dummy_result();
     301            0 :     });
     302            0 : }
     303              : 
     304            0 : Promise<Ret> AudioComService::replaceExistingAudio(DevicePtr audioData, const QString& audioFormat, const QString& title, const QUrl& url,
     305              :                                                    Visibility visibility, ProgressPtr progress)
     306              : {
     307            0 :     std::weak_ptr<QIODevice> audioDataWeakPtr = audioData; // prevents memory leak
     308              : 
     309              :     // Update visibility -> update audio info -> upload audio
     310            0 :     return doUpdateVisibility(url, visibility).then<Ret>(this,
     311            0 :                                                          [this, audioDataWeakPtr, audioFormat, title, url, visibility,
     312              :                                                           progress](const Ret& ret, auto resolve) {
     313            0 :         DevicePtr audioData = audioDataWeakPtr.lock();
     314            0 :         if (!ret || !audioData) {
     315            0 :             return resolve(ret);
     316              :         }
     317              : 
     318            0 :         doCreateAudio(title, audioData->size(), audioFormat, url, visibility, true /*replaceExisting*/)
     319            0 :         .onResolve(this, [this, audioData, audioFormat, progress, resolve](const Ret& ret) {
     320            0 :             if (!ret) {
     321            0 :                 (void)resolve(ret);
     322            0 :                 return;
     323              :             }
     324              : 
     325            0 :             doUploadAudio(audioData, audioFormat, progress).onResolve(this, [resolve](const Ret& ret) {
     326            0 :                 (void)resolve(ret);
     327              :             });
     328              :         });
     329              : 
     330            0 :         return Promise<Ret>::dummy_result();
     331            0 :     });
     332            0 : }
     333              : 
     334            0 : Promise<Ret> AudioComService::doUploadAudio(DevicePtr audioData, const QString& audioFormat, ProgressPtr progress)
     335              : {
     336            0 :     TRACEFUNC;
     337              : 
     338            0 :     return make_promise<Ret>([this, audioData, audioFormat, progress](auto resolve, auto) {
     339            0 :         IF_FAILED(!m_currentUploadingAudioInfo.isEmpty()) {
     340            0 :             return resolve(make_ret(Err::UnknownError));
     341              :         }
     342              : 
     343            0 :         QUrl url = QUrl(m_currentUploadingAudioInfo.value("url").toString());
     344            0 :         QUrl success = QUrl(m_currentUploadingAudioInfo.value("success").toString());
     345            0 :         QUrl fail = QUrl(m_currentUploadingAudioInfo.value("fail").toString());
     346            0 :         QUrl progressUrl = QUrl(m_currentUploadingAudioInfo.value("progress").toString());
     347              : 
     348            0 :         if (!url.isValid() || !success.isValid() || !fail.isValid() || !progressUrl.isValid()) {
     349            0 :             return resolve(make_ret(Err::UnknownError));
     350              :         }
     351              : 
     352            0 :         audioData->seek(0);
     353              : 
     354            0 :         QString token;
     355              : 
     356            0 :         if (m_currentUploadingAudioInfo.contains("extra")) {
     357            0 :             QJsonObject extra = m_currentUploadingAudioInfo.value("extra").toObject();
     358            0 :             QJsonObject audio = extra.value("audio").toObject();
     359            0 :             m_currentUploadingAudioSlug = audio.value("slug").toString();
     360            0 :             m_currentUploadingAudioId = audio.value("id").toString();
     361            0 :             token = extra.value("token").toString();
     362            0 :         }
     363              : 
     364            0 :         RequestHeaders headers = defaultHeaders();
     365            0 :         headers.knownHeaders[QNetworkRequest::ContentTypeHeader] = audioMime(audioFormat);
     366              : 
     367            0 :         RetVal<Progress> putProgress = m_networkManager->put(url, audioData, nullptr, headers);
     368            0 :         if (!putProgress.ret) {
     369            0 :             return resolve(putProgress.ret);
     370              :         }
     371              : 
     372            0 :         putProgress.val.progressChanged().onReceive(this, [progress](int64_t current, int64_t total, const std::string& msg) {
     373            0 :             progress->progress(current, total, msg);
     374              :         });
     375              : 
     376            0 :         putProgress.val.finished().onReceive(this, [this, token, success, fail, resolve](const ProgressResult& res) {
     377            0 :             if (res.ret) {
     378            0 :                 notifyServerAboutSuccessUpload(success, token);
     379            0 :                 (void)resolve(make_ok());
     380              :             } else {
     381            0 :                 notifyServerAboutFailUpload(fail, token);
     382            0 :                 (void)resolve(uploadingDownloadingRetFromRawRet(res.ret));
     383              :             }
     384              :         });
     385              : 
     386            0 :         return Promise<Ret>::dummy_result();
     387            0 :     });
     388            0 : }
     389              : 
     390            0 : async::Promise<Ret> AudioComService::doUpdateVisibility(const QUrl& url, Visibility visibility)
     391              : {
     392            0 :     return make_promise<Ret>([this, url, visibility](auto resolve, auto) {
     393            0 :         QUrl patchUrl(AUDIOCOM_API_ROOT_URL + "/audio/" + idFromCloudUrl(url).toQString());
     394              : 
     395            0 :         QJsonObject json;
     396            0 :         json["public"] = visibility == Visibility::Public;
     397            0 :         QByteArray jsonData = QString::fromStdString(QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact).toStdString()).toUtf8();
     398              : 
     399            0 :         auto outgoingData = std::make_shared<QBuffer>();
     400            0 :         outgoingData->setData(jsonData);
     401              : 
     402            0 :         RetVal<Progress> progress = m_networkManager->patch(patchUrl, outgoingData, nullptr, headers());
     403            0 :         if (!progress.ret) {
     404            0 :             return resolve(progress.ret);
     405              :         }
     406              : 
     407            0 :         progress.val.finished().onReceive(this, [resolve](const ProgressResult& res) {
     408            0 :             (void)resolve(res.ret);
     409              :         });
     410              : 
     411            0 :         return Promise<Ret>::dummy_result();
     412            0 :     });
     413              : }
     414              : 
     415            0 : Promise<Ret> AudioComService::doCreateAudio(const QString& title, int size,
     416              :                                             const QString& audioFormat,
     417              :                                             const QUrl& existingUrl, Visibility visibility, bool replaceExisting)
     418              : {
     419            0 :     TRACEFUNC;
     420              : 
     421            0 :     return make_promise<Ret>([this, title, size, audioFormat, existingUrl, visibility, replaceExisting](auto resolve, auto) {
     422            0 :         QJsonObject json;
     423            0 :         QString mime = audioMime(audioFormat);
     424            0 :         json["mime"] = mime;
     425            0 :         json["download_mime"] = mime;
     426            0 :         json["size"] = size;
     427            0 :         json["name"] = title;
     428            0 :         json["method"] = "PUT";
     429              : 
     430            0 :         QUrl postUrl;
     431            0 :         if (replaceExisting) {
     432            0 :             postUrl = QUrl(AUDIOCOM_API_ROOT_URL + "/audio/" + idFromCloudUrl(existingUrl).toQString() + "/source");
     433              :         } else {
     434            0 :             json["public"] = visibility == Visibility::Public;
     435            0 :             postUrl = AUDIOCOM_UPLOAD_AUDIO_API_URL;
     436              :         }
     437              : 
     438            0 :         QByteArray jsonData = QString::fromStdString(QJsonDocument(json).toJson(QJsonDocument::JsonFormat::Compact).toStdString()).toUtf8();
     439            0 :         auto outgoingData = std::make_shared<QBuffer>();
     440            0 :         outgoingData->setData(jsonData);
     441            0 :         auto receivedData = std::make_shared<QBuffer>();
     442              : 
     443            0 :         RetVal<Progress> progress = m_networkManager->post(postUrl, outgoingData, receivedData, headers());
     444            0 :         if (!progress.ret) {
     445            0 :             return resolve(progress.ret);
     446              :         }
     447              : 
     448            0 :         progress.val.finished().onReceive(this, [this, receivedData, resolve](const ProgressResult& res) {
     449            0 :             if (!res.ret) {
     450            0 :                 (void)resolve(uploadingDownloadingRetFromRawRet(res.ret));
     451            0 :                 return;
     452              :             }
     453              : 
     454            0 :             QJsonParseError err;
     455            0 :             QJsonDocument doc = QJsonDocument::fromJson(receivedData->data(), &err);
     456            0 :             if (err.error != QJsonParseError::NoError || !doc.isObject()) {
     457            0 :                 (void)resolve(muse::make_ret(Ret::Code::InternalError, err.errorString().toStdString()));
     458            0 :                 return;
     459              :             }
     460              : 
     461            0 :             m_currentUploadingAudioInfo = doc.object();
     462            0 :             (void)resolve(make_ok());
     463            0 :         });
     464              : 
     465            0 :         return Promise<Ret>::dummy_result();
     466            0 :     });
     467            0 : }
     468              : 
     469            0 : void AudioComService::notifyServerAboutFailUpload(const QUrl& failUrl, const QString& token)
     470              : {
     471            0 :     RetVal<Progress> progress = m_networkManager->del(failUrl, nullptr, headers(token));
     472            0 :     if (!progress.ret) {
     473            0 :         LOGE() << progress.ret.toString();
     474            0 :         return;
     475              :     }
     476              : 
     477            0 :     progress.val.finished().onReceive(this, [](const ProgressResult& res) {
     478            0 :         if (!res.ret) {
     479            0 :             LOGE() << res.ret.toString();
     480              :         }
     481            0 :     });
     482            0 : }
     483              : 
     484            0 : void AudioComService::notifyServerAboutSuccessUpload(const QUrl& successUrl, const QString& token)
     485              : {
     486            0 :     auto outData = std::make_shared<QBuffer>();
     487            0 :     RetVal<Progress> progress = m_networkManager->post(successUrl, outData, nullptr, headers(token));
     488            0 :     if (!progress.ret) {
     489            0 :         LOGE() << progress.ret.toString();
     490            0 :         return;
     491              :     }
     492              : 
     493            0 :     progress.val.finished().onReceive(this, [](const ProgressResult& res) {
     494            0 :         if (!res.ret) {
     495            0 :             LOGE() << res.ret.toString();
     496              :         }
     497            0 :     });
     498            0 : }
        

Generated by: LCOV version 2.0-1