LCOV - code coverage report
Current view: top level - src/esys/repo/tui - appmodel.cpp (source / functions) Hit Total Coverage
Test: esysrepo_coverage.info Lines: 137 257 53.3 %
Date: 2026-08-01 10:43:40 Functions: 16 33 48.5 %

          Line data    Source code
       1             : /*!
       2             :  * \file esys/repo/tui/appmodel.cpp
       3             :  * \brief
       4             :  *
       5             :  * \cond
       6             :  * __legal_b__
       7             :  *
       8             :  * Copyright (c) 2026 Michel Gillet
       9             :  * Distributed under the MIT License.
      10             :  * (See accompanying file LICENSE.txt or
      11             :  * copy at https://opensource.org/licenses/MIT)
      12             :  *
      13             :  * __legal_e__
      14             :  * \endcond
      15             :  *
      16             :  */
      17             : 
      18             : #include "esys/repo/esysrepo_prec.h"
      19             : #include "esys/repo/tui/appmodel.h"
      20             : #include "esys/repo/progress/repoassignedevent.h"
      21             : #include "esys/repo/progress/repodoneevent.h"
      22             : #include "esys/repo/progress/repofailedevent.h"
      23             : #include "esys/repo/progress/repoprogressevent.h"
      24             : #include "esys/repo/progress/schedulerstateevent.h"
      25             : 
      26             : #include <algorithm>
      27             : #include <variant>
      28             : 
      29             : namespace esys::repo::tui
      30             : {
      31             : 
      32           1 : const char *phase_to_string(progress::RepoPhase phase)
      33             : {
      34           1 :     switch (phase)
      35             :     {
      36             :         case progress::RepoPhase::PENDING: return "Pending";
      37           1 :         case progress::RepoPhase::RECEIVING: return "Receiving";
      38           0 :         case progress::RepoPhase::RESOLVING: return "Resolving";
      39           0 :         case progress::RepoPhase::CHECKOUT: return "Checkout";
      40           0 :         case progress::RepoPhase::DONE: return "Done";
      41           0 :         case progress::RepoPhase::FAILED: return "Failed";
      42           0 :         case progress::RepoPhase::NOT_SET:
      43           0 :         default: return "";
      44             :     }
      45             : }
      46             : 
      47           0 : const char *queue_reason_to_string(QueueReason reason)
      48             : {
      49           0 :     switch (reason)
      50             :     {
      51             :         case QueueReason::EMPTY: return "empty";
      52           0 :         case QueueReason::PAUSED: return "paused";
      53           0 :         case QueueReason::BACKPRESSURE: return "backpressure";
      54           0 :         case QueueReason::WAITING_FOR_WORKER: return "waiting for worker";
      55           0 :         case QueueReason::RUNNING: return "running";
      56             :     }
      57           0 :     return "";
      58             : }
      59             : 
      60           0 : const char *queue_filter_to_string(QueueFilter filter)
      61             : {
      62           0 :     switch (filter)
      63             :     {
      64             :         case QueueFilter::ALL: return "all";
      65           0 :         case QueueFilter::PENDING: return "pending";
      66           0 :         case QueueFilter::RUNNING: return "running";
      67           0 :         case QueueFilter::FAILED: return "failed";
      68           0 :         case QueueFilter::DONE: return "done";
      69             :     }
      70           0 :     return "";
      71             : }
      72             : 
      73           1 : AppModel::AppModel() = default;
      74             : 
      75           1 : AppModel::~AppModel() = default;
      76             : 
      77           2 : void AppModel::reset(std::size_t repo_count, std::size_t worker_count)
      78             : {
      79           2 :     m_state.get_repos().assign(repo_count, RepoState{});
      80           2 :     m_state.get_workers().clear();
      81           2 :     m_state.get_workers().reserve(worker_count);
      82           5 :     for (std::size_t i = 0; i < worker_count; ++i)
      83           3 :         m_state.get_workers().emplace_back(static_cast<int>(i));
      84             : 
      85           2 :     m_state.set_selected_worker(0);
      86           2 :     m_state.set_selected_repo(0);
      87           2 :     m_state.set_scheduler_paused(false);
      88           2 :     m_state.set_scheduler_backpressure(false);
      89           2 :     m_state.set_scheduler_pending_queue(0);
      90           2 :     m_state.set_scheduler_active_workers(0);
      91           2 :     m_state.set_queue_filter(QueueFilter::ALL);
      92           2 :     recompute_counters();
      93           2 : }
      94             : 
      95           0 : void AppModel::reset(const std::vector<std::string> &repo_names, std::size_t worker_count)
      96             : {
      97           0 :     reset(repo_names.size(), worker_count);
      98           0 :     for (std::size_t i = 0; i < repo_names.size(); ++i)
      99             :     {
     100           0 :         m_state.get_repos()[i].set_name(repo_names[i]);
     101           0 :         m_state.get_repos()[i].set_path(repo_names[i]);
     102             :     }
     103           0 : }
     104             : 
     105           9 : void AppModel::apply(const progress::SyncEvent &event)
     106             : {
     107          18 :     std::visit([this](const auto &e) { apply_impl(e); }, event);
     108           9 :     recompute_counters();
     109           9 : }
     110             : 
     111           1 : std::size_t AppModel::drain_and_apply(EventQueue &queue)
     112             : {
     113           1 :     std::vector<progress::SyncEvent> events;
     114           1 :     queue.drain(events);
     115           3 :     for (const auto &event : events) apply(event);
     116           1 :     sync_log_buffer();
     117           1 :     return events.size();
     118           1 : }
     119             : 
     120           1 : void AppModel::sync_log_buffer()
     121             : {
     122           1 :     if (m_log_buffer == nullptr) return;
     123           0 :     m_state.set_log_lines(m_log_buffer->snapshot());
     124           0 :     if (m_state.get_log_follow_tail())
     125             :     {
     126           0 :         const int height = m_state.get_log_pane_height();
     127           0 :         const int n = static_cast<int>(m_state.get_log_lines().size());
     128           0 :         const int max_off = (std::max)(0, n - height);
     129           0 :         m_state.set_log_scroll_offset(max_off);
     130             :     }
     131             :     else
     132           0 :         clamp_log_scroll();
     133             : }
     134             : 
     135           0 : void AppModel::set_log_buffer(std::shared_ptr<LogBuffer> log_buffer)
     136             : {
     137           0 :     m_log_buffer = std::move(log_buffer);
     138           0 : }
     139             : 
     140           0 : std::shared_ptr<LogBuffer> AppModel::get_log_buffer() const
     141             : {
     142           0 :     return m_log_buffer;
     143             : }
     144             : 
     145           0 : void AppModel::set_phase_text(std::string phase_text)
     146             : {
     147           0 :     m_state.set_phase_text(std::move(phase_text));
     148           0 : }
     149             : 
     150           0 : void AppModel::clamp_log_scroll()
     151             : {
     152           0 :     const int height = m_state.get_log_pane_height();
     153           0 :     const int n = static_cast<int>(m_state.get_log_lines().size());
     154           0 :     const int max_off = (std::max)(0, n - height);
     155           0 :     int off = m_state.get_log_scroll_offset();
     156           0 :     if (off < 0) off = 0;
     157           0 :     if (off > max_off) off = max_off;
     158           0 :     m_state.set_log_scroll_offset(off);
     159           0 :     if (off >= max_off) m_state.set_log_follow_tail(true);
     160           0 : }
     161             : 
     162           0 : void AppModel::scroll_log_page_up()
     163             : {
     164           0 :     sync_log_buffer();
     165           0 :     const int height = (std::max)(1, m_state.get_log_pane_height());
     166           0 :     m_state.set_log_follow_tail(false);
     167           0 :     m_state.set_log_scroll_offset(m_state.get_log_scroll_offset() - height);
     168           0 :     clamp_log_scroll();
     169           0 : }
     170             : 
     171           0 : void AppModel::scroll_log_page_down()
     172             : {
     173           0 :     sync_log_buffer();
     174           0 :     const int height = (std::max)(1, m_state.get_log_pane_height());
     175           0 :     m_state.set_log_scroll_offset(m_state.get_log_scroll_offset() + height);
     176           0 :     clamp_log_scroll();
     177           0 : }
     178             : 
     179          39 : AppState &AppModel::get_state()
     180             : {
     181          39 :     return m_state;
     182             : }
     183             : 
     184           0 : const AppState &AppModel::get_state() const
     185             : {
     186           0 :     return m_state;
     187             : }
     188             : 
     189           2 : void AppModel::apply_impl(const progress::RepoAssignedEvent &event)
     190             : {
     191           2 :     auto &repo = m_state.get_repos().at(event.get_repo_index());
     192           2 :     auto &worker = m_state.get_workers().at(static_cast<std::size_t>(event.get_worker_id()));
     193             : 
     194           2 :     clear_worker_assignment(repo);
     195             : 
     196           2 :     if (worker.get_repo_index().has_value())
     197             :     {
     198           0 :         const std::size_t prev_repo = *worker.get_repo_index();
     199           0 :         if (prev_repo != event.get_repo_index() && prev_repo < m_state.get_repos().size())
     200             :         {
     201           0 :             auto &prev = m_state.get_repos()[prev_repo];
     202           0 :             if (prev.get_assigned_worker_id() == event.get_worker_id())
     203           0 :                 prev.set_assigned_worker_id(std::nullopt);
     204             :         }
     205             :     }
     206             : 
     207           2 :     repo.set_status(RepoStatus::RUNNING);
     208           2 :     repo.set_assigned_worker_id(event.get_worker_id());
     209           2 :     repo.set_phase_text("Pending");
     210           2 :     repo.set_error_text("");
     211             : 
     212           2 :     worker.set_busy(true);
     213           2 :     worker.set_repo_index(event.get_repo_index());
     214           2 :     worker.set_errored(false);
     215           2 :     worker.set_last_event("assigned");
     216           2 : }
     217             : 
     218           1 : void AppModel::apply_impl(const progress::RepoProgressEvent &event)
     219             : {
     220           1 :     auto &repo = m_state.get_repos().at(event.get_repo_index());
     221           1 :     repo.set_phase_text(phase_to_string(event.get_phase()));
     222           1 :     repo.set_progress(event.get_counters());
     223           1 :     repo.set_detail_text(event.get_detail());
     224           1 :     repo.set_status(RepoStatus::RUNNING);
     225             : 
     226           1 :     if (repo.get_assigned_worker_id().has_value())
     227             :     {
     228           1 :         const int wid = *repo.get_assigned_worker_id();
     229           1 :         if (wid >= 0 && static_cast<std::size_t>(wid) < m_state.get_workers().size())
     230           2 :             m_state.get_workers()[static_cast<std::size_t>(wid)].set_last_event("progress");
     231             :     }
     232           1 : }
     233             : 
     234           2 : void AppModel::apply_impl(const progress::RepoDoneEvent &event)
     235             : {
     236           2 :     auto &repo = m_state.get_repos().at(event.get_repo_index());
     237             :     // Fast repos may never emit progress; still show Rx/Idx/Chk at 100% when Done.
     238           2 :     auto counters = repo.get_progress();
     239           2 :     counters.mark_complete();
     240           2 :     repo.set_progress(counters);
     241           2 :     clear_worker_assignment(repo);
     242           2 :     repo.set_status(RepoStatus::DONE);
     243           2 :     repo.set_phase_text("Done");
     244           2 :     repo.set_detail_text("completed successfully");
     245           2 :     repo.set_error_text("");
     246           2 : }
     247             : 
     248           1 : void AppModel::apply_impl(const progress::RepoFailedEvent &event)
     249             : {
     250           1 :     auto &repo = m_state.get_repos().at(event.get_repo_index());
     251           1 :     if (repo.get_assigned_worker_id().has_value())
     252             :     {
     253           1 :         const int wid = *repo.get_assigned_worker_id();
     254           1 :         if (wid >= 0 && static_cast<std::size_t>(wid) < m_state.get_workers().size())
     255             :         {
     256           1 :             auto &worker = m_state.get_workers()[static_cast<std::size_t>(wid)];
     257           1 :             worker.set_errored(true);
     258           2 :             worker.set_last_event("failed");
     259             :         }
     260             :     }
     261           1 :     clear_worker_assignment(repo);
     262           1 :     repo.set_status(RepoStatus::FAILED);
     263           1 :     repo.set_phase_text("Failed");
     264           1 :     repo.set_error_text(event.get_error());
     265           1 : }
     266             : 
     267           3 : void AppModel::apply_impl(const progress::SchedulerStateEvent &event)
     268             : {
     269           3 :     m_state.set_scheduler_paused(event.get_paused());
     270           3 :     m_state.set_scheduler_backpressure(event.get_backpressure());
     271           3 :     m_state.set_scheduler_pending_queue(event.get_pending_queue());
     272           3 :     m_state.set_scheduler_active_workers(event.get_active_workers());
     273           3 : }
     274             : 
     275           5 : void AppModel::clear_worker_assignment(RepoState &repo)
     276             : {
     277           5 :     if (!repo.get_assigned_worker_id().has_value()) return;
     278             : 
     279           2 :     const int wid = *repo.get_assigned_worker_id();
     280           2 :     if (wid >= 0 && static_cast<std::size_t>(wid) < m_state.get_workers().size())
     281             :     {
     282           2 :         auto &worker = m_state.get_workers()[static_cast<std::size_t>(wid)];
     283           2 :         worker.set_busy(false);
     284           2 :         worker.set_repo_index(std::nullopt);
     285             :         // Failure belongs on the repo row; do not keep marking idle slots Failed.
     286           2 :         worker.set_errored(false);
     287             :     }
     288           2 :     repo.set_assigned_worker_id(std::nullopt);
     289             : }
     290             : 
     291          11 : void AppModel::recompute_counters()
     292             : {
     293          11 :     int done = 0;
     294          11 :     int running = 0;
     295          11 :     int pending = 0;
     296          11 :     int failed = 0;
     297             : 
     298          37 :     for (const auto &repo : m_state.get_repos())
     299             :     {
     300          26 :         switch (repo.get_status())
     301             :         {
     302           2 :             case RepoStatus::DONE: ++done; break;
     303           3 :             case RepoStatus::RUNNING: ++running; break;
     304           5 :             case RepoStatus::FAILED: ++failed; break;
     305          16 :             case RepoStatus::PENDING:
     306          16 :             case RepoStatus::SKIPPED:
     307          16 :             default: ++pending; break;
     308             :         }
     309             :     }
     310             : 
     311          11 :     m_state.set_done_count(done);
     312          11 :     m_state.set_running_count(running);
     313          11 :     m_state.set_pending_count(pending);
     314          11 :     m_state.set_failed_count(failed);
     315          11 :     recompute_queue_reason();
     316          11 : }
     317             : 
     318          11 : void AppModel::recompute_queue_reason()
     319             : {
     320             :     // Priority matches ADR-0018: paused / backpressure / empty / waiting / running.
     321          11 :     if (m_state.get_scheduler_paused())
     322             :     {
     323           1 :         m_state.set_queue_reason(QueueReason::PAUSED);
     324           1 :         return;
     325             :     }
     326          10 :     if (m_state.get_scheduler_backpressure())
     327             :     {
     328           1 :         m_state.set_queue_reason(QueueReason::BACKPRESSURE);
     329           1 :         return;
     330             :     }
     331           9 :     if (m_state.get_pending_count() == 0 && m_state.get_running_count() == 0)
     332             :     {
     333           1 :         m_state.set_queue_reason(QueueReason::EMPTY);
     334           1 :         return;
     335             :     }
     336             : 
     337           8 :     bool any_idle_worker = false;
     338           8 :     bool any_busy_worker = false;
     339          20 :     for (const auto &worker : m_state.get_workers())
     340             :     {
     341          12 :         if (worker.get_busy()) any_busy_worker = true;
     342           9 :         else any_idle_worker = true;
     343             :     }
     344             : 
     345           8 :     if (m_state.get_pending_count() > 0 && any_busy_worker && !any_idle_worker)
     346             :     {
     347           1 :         m_state.set_queue_reason(QueueReason::WAITING_FOR_WORKER);
     348           1 :         return;
     349             :     }
     350             : 
     351           7 :     m_state.set_queue_reason(QueueReason::RUNNING);
     352             : }
     353             : 
     354           0 : void AppModel::set_layout_mode(LayoutMode layout_mode)
     355             : {
     356           0 :     m_state.set_layout_mode(layout_mode);
     357           0 : }
     358             : 
     359           0 : void AppModel::cycle_layout_mode()
     360             : {
     361             :     // Sequence: AUTO -> COMPACT -> BALANCED -> DETAILED -> QUEUE_FIRST -> AUTO
     362           0 :     switch (m_state.get_layout_mode())
     363             :     {
     364           0 :         case LayoutMode::AUTO: m_state.set_layout_mode(LayoutMode::COMPACT); break;
     365           0 :         case LayoutMode::COMPACT: m_state.set_layout_mode(LayoutMode::BALANCED); break;
     366           0 :         case LayoutMode::BALANCED: m_state.set_layout_mode(LayoutMode::DETAILED); break;
     367           0 :         case LayoutMode::DETAILED: m_state.set_layout_mode(LayoutMode::QUEUE_FIRST); break;
     368           0 :         case LayoutMode::QUEUE_FIRST:
     369           0 :         default: m_state.set_layout_mode(LayoutMode::AUTO); break;
     370             :     }
     371           0 : }
     372             : 
     373           0 : void AppModel::set_selected_repo(int repo_idx)
     374             : {
     375           0 :     const int n = static_cast<int>(m_state.get_repos().size());
     376           0 :     if (n <= 0)
     377             :     {
     378           0 :         m_state.set_selected_repo(0);
     379           0 :         return;
     380             :     }
     381             : 
     382           0 :     int idx = repo_idx;
     383           0 :     if (idx < 0) idx = 0;
     384           0 :     if (idx >= n) idx = n - 1;
     385           0 :     m_state.set_selected_repo(idx);
     386             : }
     387             : 
     388           0 : void AppModel::set_selected_worker(int worker_idx)
     389             : {
     390           0 :     const int n = static_cast<int>(m_state.get_workers().size());
     391           0 :     if (n <= 0)
     392             :     {
     393           0 :         m_state.set_selected_worker(0);
     394           0 :         return;
     395             :     }
     396             : 
     397           0 :     int idx = worker_idx;
     398           0 :     if (idx < 0) idx = 0;
     399           0 :     if (idx >= n) idx = n - 1;
     400           0 :     m_state.set_selected_worker(idx);
     401             : 
     402             :     // Keep selected_repo aligned with selected worker's current assignment, when possible.
     403           0 :     const auto &workers = m_state.get_workers();
     404           0 :     if (idx < static_cast<int>(workers.size()) && workers[idx].get_repo_index().has_value())
     405           0 :         set_selected_repo(static_cast<int>(*workers[idx].get_repo_index()));
     406             : }
     407             : 
     408           0 : void AppModel::set_terminal_size(int terminal_cols, int terminal_rows)
     409             : {
     410           0 :     m_state.set_terminal_cols(terminal_cols);
     411           0 :     m_state.set_terminal_rows(terminal_rows);
     412           0 : }
     413             : 
     414           0 : void AppModel::set_queue_filter(QueueFilter queue_filter)
     415             : {
     416           0 :     m_state.set_queue_filter(queue_filter);
     417           0 : }
     418             : 
     419           0 : void AppModel::cycle_queue_filter()
     420             : {
     421           0 :     switch (m_state.get_queue_filter())
     422             :     {
     423           0 :         case QueueFilter::ALL: m_state.set_queue_filter(QueueFilter::PENDING); break;
     424           0 :         case QueueFilter::PENDING: m_state.set_queue_filter(QueueFilter::RUNNING); break;
     425           0 :         case QueueFilter::RUNNING: m_state.set_queue_filter(QueueFilter::FAILED); break;
     426           0 :         case QueueFilter::FAILED: m_state.set_queue_filter(QueueFilter::DONE); break;
     427           0 :         case QueueFilter::DONE:
     428           0 :         default: m_state.set_queue_filter(QueueFilter::ALL); break;
     429             :     }
     430           0 : }
     431             : 
     432             : } // namespace esys::repo::tui

Generated by: LCOV version 1.14