Add StatusTable class
This encapsulate the logic used to print the pending commands
table on smart terminals. A future CL will modify the Status
class to use this instead.
Fuchsia-Topic: multiline-status
Change-Id: I282cd0287bb143877569f4f0d77bdb8ffebd8271
Reviewed-on: https://fuchsia-review.googlesource.com/c/third_party/github.com/ninja-build/ninja/+/1037597
Commit-Queue: David Turner <digit@google.com>
Reviewed-by: David Fang <fangism@google.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f4c3145..7323e50 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -151,6 +151,7 @@
src/process_utils.cc
src/state.cc
src/status_printer.cc
+ src/status_table.cc
src/stdio_redirection.cc
src/string_piece_util.cc
src/util.cc
@@ -319,6 +320,7 @@
src/persistent_service_test.cc
src/process_utils_test.cc
src/stat_cache_test.cc
+ src/status_table_test.cc
src/state_test.cc
src/stdio_redirection_test.cc
src/string_piece_util_test.cc
diff --git a/configure.py b/configure.py
index cd9584a..d611257 100755
--- a/configure.py
+++ b/configure.py
@@ -547,6 +547,7 @@
'process_utils',
'state',
'status_printer',
+ 'status_table',
'stdio_redirection',
'string_piece_util',
'util',
diff --git a/src/status_table.cc b/src/status_table.cc
new file mode 100644
index 0000000..8c31d0b
--- /dev/null
+++ b/src/status_table.cc
@@ -0,0 +1,217 @@
+// Copyright 2024 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "status_table.h"
+
+#include <algorithm>
+
+#include "assert.h"
+
+#ifdef _WIN32
+#undef min // make std::min() work on Windows.
+#endif
+
+StatusTable::StatusTable(const Config& config, AsyncLoop& async_loop)
+ : config_(config), async_loop_(async_loop) {}
+
+StatusTable::~StatusTable() = default;
+
+void StatusTable::BuildStarted() {
+ start_build_time_ms_ = async_loop_.NowMs();
+
+ if (config_.refresh_timeout_ms > 0) {
+ if (!timer_) {
+ timer_ = AsyncTimer::CreateWithDuration(
+ config_.refresh_timeout_ms, async_loop_, [this] {
+ int64_t cur_time_ms = async_loop_.NowMs() - start_build_time_ms_;
+ Refresh(cur_time_ms);
+ timer_.SetDurationMs(config_.refresh_timeout_ms);
+ });
+ }
+ EnableTimer();
+ }
+}
+
+void StatusTable::BuildEnded() {
+ if (timer_) {
+ timer_.Close();
+ }
+ start_build_time_ms_ = 0;
+ last_update_time_ms_ = -1;
+ ClearTable();
+}
+
+void StatusTable::CommandStarted(CommandPointer command) {
+ pending_commands_[command] = async_loop_.NowMs() - start_build_time_ms_;
+}
+
+void StatusTable::CommandEnded(CommandPointer command) {
+ auto it = pending_commands_.find(command);
+ assert(it != pending_commands_.end());
+ pending_commands_.erase(it);
+}
+
+void StatusTable::SetStatus(const std::string& status) {
+ last_status_ = status;
+}
+
+void StatusTable::UpdateTable() {
+ Refresh(async_loop_.NowMs());
+}
+
+void StatusTable::EnableTimer() {
+ if (timer_)
+ timer_.SetDurationMs(config_.refresh_timeout_ms);
+}
+
+void StatusTable::DisableTimer() {
+ if (timer_)
+ timer_.Cancel();
+}
+
+void StatusTable::Refresh(int64_t current_time_ms) {
+ if (last_update_time_ms_ >= 0) {
+ int64_t since_last_ms = current_time_ms - last_update_time_ms_;
+ if (since_last_ms < config_.refresh_timeout_ms) {
+ // No need to update more than necessary when tasks complete
+ // really really fast.
+ return;
+ }
+ }
+ last_update_time_ms_ = current_time_ms;
+ PrintPending(current_time_ms);
+}
+
+void StatusTable::PrintPending(int64_t cur_time_millis) {
+ if (!config_.max_commands)
+ return;
+
+ // Find the N-th older running edges, where N is max_height_.
+ // Reuse the sorted_pending_edges_ vector between calls.
+ auto& sorted_commands = sorted_pending_commands_;
+ sorted_commands.assign(pending_commands_.begin(), pending_commands_.end());
+
+ auto less = [](const CommandInfo& a, const CommandInfo& b) -> bool {
+ return a.second < b.second;
+ };
+ size_t count = std::min(sorted_commands.size(), config_.max_commands);
+
+ std::partial_sort(sorted_commands.begin(), sorted_commands.begin() + count,
+ sorted_commands.end(), less);
+
+ std::string pending_line;
+ for (size_t n = 0; n < count; ++n) {
+ CommandInfo& pair = sorted_commands[n];
+
+ // Format the elapsed time in a human friendly format.
+ char elapsed_buffer[16];
+ int64_t elapsed_ms = cur_time_millis - pair.second;
+ if (elapsed_ms < 0) {
+ snprintf(elapsed_buffer, sizeof(elapsed_buffer), "??????");
+ } else {
+ if (elapsed_ms < 60000) {
+ snprintf(elapsed_buffer, sizeof(elapsed_buffer), "%d.%ds",
+ static_cast<int>((elapsed_ms / 1000)),
+ static_cast<int>((elapsed_ms % 1000) / 100));
+ } else {
+ snprintf(elapsed_buffer, sizeof(elapsed_buffer), "%dm%ds",
+ static_cast<int>((elapsed_ms / 60000)),
+ static_cast<int>((elapsed_ms % 60000) / 1000));
+ }
+ }
+
+ // Get edge description or command.
+ std::string description = GetCommandDescription(pair.first);
+
+ // Format '<elapsed> | <description>' where <elapsed> is
+ // right-justified.
+ size_t justification_width = 6;
+ size_t elapsed_width = strlen(elapsed_buffer);
+ size_t justified_elapsed_width =
+ std::min(justification_width, elapsed_width);
+ size_t needed_capacity = justified_elapsed_width + 3 + description.size();
+ if (needed_capacity > pending_line.capacity())
+ pending_line.reserve(needed_capacity);
+ if (elapsed_width < justification_width) {
+ pending_line.assign(justification_width - elapsed_width, ' ');
+ } else {
+ pending_line.clear();
+ }
+ pending_line.append(elapsed_buffer, elapsed_width);
+ pending_line.append(" | ", 3);
+ pending_line.append(description);
+
+ PrintOnNextLine(pending_line);
+ }
+
+ // Clear previous lines that are not needed anymore.
+ size_t next_height = count;
+ for (; count < last_command_count_; ++count) {
+ ClearNextLine();
+ }
+
+ if (count > 0) {
+ // Move up to the top status line. Then print the status
+ // again to reposition the cursor to the right position.
+ // Note that using ASCII sequences to save/restore the
+ // cursor position does not work reliably in all terminals
+ // (and terminal emulators like mosh or asciinema).
+ MoveUp(count);
+ PrintOnCurrentLine(last_status_);
+ }
+ Flush();
+
+ last_command_count_ = next_height;
+}
+
+std::string StatusTable::GetCommandDescription(
+ StatusTable::CommandPointer command) const {
+ return "command $" + std::to_string(reinterpret_cast<uintptr_t>(command));
+}
+
+void StatusTable::PrintOnCurrentLine(const std::string& line) {
+ printf("%s\x1B[0K", line.c_str());
+}
+
+void StatusTable::PrintOnNextLine(const std::string& line) {
+ printf("\n");
+ PrintOnCurrentLine(line);
+}
+
+void StatusTable::ClearNextLine() {
+ printf("\x1B[1B\x1B[2K");
+}
+
+void StatusTable::MoveUp(size_t lines_count) {
+ printf("\x1B[%dA", static_cast<int>(lines_count));
+}
+
+void StatusTable::Flush() {
+ fflush(stdout);
+}
+
+void StatusTable::ClearTable() {
+ if (last_command_count_ == 0)
+ return;
+
+ // repeat "go down 1 line; erase whole line" |last_height_| times.
+ for (size_t n = 0; n < last_command_count_; ++n)
+ ClearNextLine();
+
+ // move up |last_height_| lines.
+ MoveUp(last_command_count_);
+ Flush();
+
+ last_command_count_ = 0;
+}
diff --git a/src/status_table.h b/src/status_table.h
new file mode 100644
index 0000000..dbc27c2
--- /dev/null
+++ b/src/status_table.h
@@ -0,0 +1,187 @@
+// Copyright 2024 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NINJA_STATUS_TABLE_H_
+#define NINJA_STATUS_TABLE_H_
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "async_loop.h"
+
+/// A class used to display a table of pending commands during
+/// the build on smart terminals, using ANSI sequences whenever possible.
+///
+/// Concrete implementations should override the GetCommandDescription()
+/// and PrintOnCurrentLine() method to work on smart terminals.
+///
+/// Usage is the following:
+///
+/// 1) Create instance, passing configuration information and
+/// a valid AsyncLoop reference.
+///
+/// 2) Call `BuildStarted()` when the build starts. Similarly, call
+/// `BuildEnded()` when it stops.
+///
+/// 3) Call `CommandStarted(command)` whenever a new command is started, where
+/// |command| is a unique pointer value for the command, whose description
+/// can be returned by `GetCommandDescription(command)`.
+///
+/// 4) Call `CommandEnded(command)` when a given command ended. This does not
+/// update the table, only internal counters.
+///
+/// 5) Call `SetStatus(status_line)` whenever the status line changes.
+/// It is needed to restore the cursor position after printing the table,
+/// unfortunately (there is no good way to save cursor positions with
+/// various terminal emulators).
+///
+/// 6) Call `ClearTable()` to clear the table (e.g. before printing something,
+/// or at the end of the build). Call `UpdateTable()` to print the table
+/// if needed (e.g. if it was cleared previously, or if enough time has
+/// passed since the last call).
+///
+class StatusTable {
+ public:
+ /// An opaque type describing a given unique command.
+ using CommandPointer = const void*;
+
+ /// Configuration information for a new StatusTable instance.
+ ///
+ /// |max_commands| is the maximum number of commands to print in the table.
+ /// A value of 0 completely disables the feature.
+ ///
+ /// |refresh_timeout_ms| is the periodic refresh timeout used by the
+ /// internal timer. A negative value disables the feature, and the table
+ /// will only be updated on ClearTable() and UpdateTable() calls.
+ ///
+ struct Config {
+ size_t max_commands = 0;
+ int64_t refresh_timeout_ms = -1;
+ };
+
+ /// Constructor. |max_commands| is the maximum number of commands to print
+ /// (a value of 0 disables the feature). |refresh_timeout_ms| is the minimum
+ /// refresh timeout (a value of 0 disables the timer). The |async_loop|
+ /// reference is used to create a timer for periodic updates.
+ StatusTable(const Config& config, AsyncLoop& async_loop);
+
+ /// Destructor.
+ virtual ~StatusTable();
+
+ /// Call this when starting a new build.
+ void BuildStarted();
+
+ /// Call this when a new command is starting.
+ void CommandStarted(CommandPointer command);
+
+ /// Call this when a started command completes.
+ /// It is a runtime error to use a |command| value that does not
+ /// match a previous CommandStart() call, that was not previously
+ /// finished.
+ void CommandEnded(CommandPointer command);
+
+ /// Call this when the build has completed.
+ void BuildEnded();
+
+ /// Call this to update the status at the top of the table, and update
+ /// the commands below it if needed.
+ void SetStatus(const std::string& status);
+
+ /// Call this to clear the table, if any.
+ void ClearTable();
+
+ /// Call this to update the table if needed.
+ void UpdateTable();
+
+ /// Disable periodic timer updates. Only PrintTable() and ClearTable()
+ /// will update the output from now on.
+ void DisableTimer();
+
+ /// Enable periodic updates through AsyncLoop timers. This ensures the
+ /// table is automatically updated every config.refresh_timeout_ms
+ /// milliseconds if there are no calls to UpdateTable() or ClearTable()
+ /// in between.
+ void EnableTimer();
+
+ protected:
+ /// The following methods can be overriden by sub-classes.
+ /// Both GetCommandDescription() and PrintOnCurrentLine() should be
+ /// overridden for proper command output in smart terminals.
+
+ /// Return a string describing a command.
+ /// This method should be overridden by derived classes, as the default
+ /// simply returns "command <number>".
+ virtual std::string GetCommandDescription(CommandPointer command) const;
+
+ /// Print |line| from the start of the current line, and place the cursor
+ /// right after it, clearing anything after it. This must *not* print more
+ /// than the terminal width, nor move the cursor to the next line.
+ ///
+ /// This method should be overridden by derived classes, as the default
+ /// prints on the current line without trying to limit the width, then
+ /// does an ANSI "erase from cursor to end of line" sequence.
+ virtual void PrintOnCurrentLine(const std::string& line);
+
+ /// The following methods can be overriden for tests. Their default behavior
+ /// is to use standard ANSI sequences, and eventually the above two methods.
+
+ /// Jump to the next line, then print |line| on it just like
+ /// PrintOnCurrentLine.
+ virtual void PrintOnNextLine(const std::string& line);
+
+ /// Move down to the next line, then clear it completely. The cursor can
+ /// stay on the same column.
+ virtual void ClearNextLine();
+
+ /// Move up |lines_count| lines. The cursor can stay on the same column.
+ virtual void MoveUp(size_t lines_count);
+
+ /// Flush all previous commands to final terminal.
+ virtual void Flush();
+
+ private:
+ /// Called to refresh the pending list if needed.
+ void Refresh(int64_t current_time_ms);
+
+ /// Support for printing pending commands below the status on smart terminals.
+ void PrintPending(int64_t cur_time_millis);
+
+ Config config_;
+ size_t last_command_count_ = 0;
+ int64_t start_build_time_ms_ = 0;
+ int64_t last_update_time_ms_ = -1;
+ std::string last_status_;
+
+ AsyncLoop& async_loop_;
+ AsyncTimer timer_;
+
+ // Record pending commands. This maps an opaque command pointer
+ // to its start time in milliseconds.
+ using CommandMap = std::unordered_map<CommandPointer, int64_t>;
+ CommandMap pending_commands_;
+
+ // Used on each PrintPending() call to minimize heap allocations.
+ // Note that CommandMap::value_type.first has type |const Edge* const| and
+ // thus CommandMap::value_type is not copyable and cannot be used as an
+ // std::vector<> item type.
+ using CommandInfo = std::pair<CommandPointer, int64_t>;
+ std::vector<CommandInfo> sorted_pending_commands_;
+};
+
+#endif // NINJA_STATUS_TABLE_H_
diff --git a/src/status_table_test.cc b/src/status_table_test.cc
new file mode 100644
index 0000000..9e87520
--- /dev/null
+++ b/src/status_table_test.cc
@@ -0,0 +1,237 @@
+// Copyright 2024 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "status_table.h"
+
+#include "async_loop.h"
+#include "test.h"
+#include "util.h"
+
+/// A StatusTable sub-class that just appends method calls to a log string.
+struct TestStatusTable : public StatusTable {
+ TestStatusTable(const StatusTable::Config& config, AsyncLoop& async_loop)
+ : StatusTable(config, async_loop) {}
+
+ std::string GetCommandDescription(CommandPointer command) const override {
+ return StringFormat("command_%zu", reinterpret_cast<size_t>(command));
+ }
+
+ void PrintOnCurrentLine(const std::string& line) override {
+ StringAppendFormat(log_, "PrintOnCurrentLine(%s)\n", line.c_str());
+ }
+
+ void PrintOnNextLine(const std::string& line) override {
+ StringAppendFormat(log_, "PrintOnNextLine(%s)\n", line.c_str());
+ }
+
+ void ClearNextLine() override { log_ += "ClearNextLine()\n"; }
+
+ void MoveUp(size_t line_count) override {
+ StringAppendFormat(log_, "MoveUp(%zd)\n", line_count);
+ }
+
+ void Flush() override { log_ += "Flush()\n"; }
+
+ static StatusTable::CommandPointer MakeCommand(size_t n) {
+ return reinterpret_cast<StatusTable::CommandPointer>(n);
+ }
+
+ // Return log (and clear it as well in the instance)
+ std::string log() { return std::move(log_); }
+
+ mutable std::string log_;
+};
+
+TEST(StatusTable, NoCommands) {
+ StatusTable::Config table_config = { 0, 100 };
+ AsyncLoop& loop = AsyncLoop::Get();
+ AsyncLoop::ScopedTestClock clock(loop);
+ TestStatusTable table(table_config, loop);
+ ASSERT_EQ(table.log(), "");
+
+ table.SetStatus("some_status");
+ table.BuildStarted();
+
+ auto cmd1 = table.MakeCommand(1);
+ auto cmd2 = table.MakeCommand(2);
+ auto cmd3 = table.MakeCommand(3);
+
+ table.CommandStarted(cmd1);
+ table.CommandStarted(cmd2);
+ table.CommandStarted(cmd3);
+ table.UpdateTable();
+
+ ASSERT_EQ(table.log(), "");
+
+ clock.AdvanceTimeMillis(100);
+ table.CommandEnded(cmd2);
+
+ clock.AdvanceTimeMillis(400);
+ table.CommandEnded(cmd3);
+ table.UpdateTable();
+
+ ASSERT_EQ(table.log(), "");
+
+ table.BuildEnded();
+ ASSERT_EQ(table.log(), "");
+}
+
+TEST(StatusTable, TwoCommandsNoPeriodicUpdates) {
+ StatusTable::Config table_config = { 2, -1 };
+ AsyncLoop& loop = AsyncLoop::Get();
+ AsyncLoop::ScopedTestClock clock(loop);
+ TestStatusTable table(table_config, loop);
+ ASSERT_EQ(table.log(), "");
+
+ table.SetStatus("some_status");
+ table.BuildStarted();
+
+ auto cmd1 = table.MakeCommand(1);
+ auto cmd2 = table.MakeCommand(2);
+ auto cmd3 = table.MakeCommand(3);
+
+ table.CommandStarted(cmd1);
+ clock.AdvanceTimeMillis(250);
+ table.CommandStarted(cmd2);
+ clock.AdvanceTimeMillis(320);
+ table.CommandStarted(cmd3);
+ table.UpdateTable();
+
+ ASSERT_EQ(table.log(),
+ "PrintOnNextLine( 0.5s | command_1)\n"
+ "PrintOnNextLine( 0.3s | command_2)\n"
+ "MoveUp(2)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ clock.AdvanceTimeMillis(100);
+ table.CommandEnded(cmd1);
+ table.UpdateTable();
+ ASSERT_EQ(table.log(),
+ "PrintOnNextLine( 0.4s | command_2)\n"
+ "PrintOnNextLine( 0.1s | command_3)\n"
+ "MoveUp(2)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ clock.AdvanceTimeMillis(400);
+ table.CommandEnded(cmd2);
+ table.UpdateTable();
+
+ ASSERT_EQ(table.log(),
+ "PrintOnNextLine( 0.5s | command_3)\n"
+ "ClearNextLine()\n"
+ "MoveUp(2)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ table.CommandEnded(cmd3);
+ table.UpdateTable();
+ ASSERT_EQ(table.log(),
+ "ClearNextLine()\n"
+ "MoveUp(1)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ table.BuildEnded();
+ table.UpdateTable();
+ ASSERT_EQ(table.log_, "Flush()\n");
+}
+
+TEST(StatusTable, TwoCommandsWithPeriodicUpdates) {
+ StatusTable::Config table_config = { 2, 100 };
+ AsyncLoop& loop = AsyncLoop::Get();
+ AsyncLoop::ScopedTestClock clock(loop);
+ TestStatusTable table(table_config, loop);
+ ASSERT_EQ(table.log(), "");
+
+ table.SetStatus("some_status");
+ table.BuildStarted();
+
+ auto cmd1 = table.MakeCommand(1);
+ auto cmd2 = table.MakeCommand(2);
+ auto cmd3 = table.MakeCommand(3);
+
+ table.CommandStarted(cmd1);
+ clock.AdvanceTimeMillis(250); // time == 250
+ table.CommandStarted(cmd2);
+ clock.AdvanceTimeMillis(320); // time == 570
+ table.CommandStarted(cmd3);
+ table.UpdateTable();
+
+ ASSERT_EQ(table.log(),
+ "PrintOnNextLine( 0.5s | command_1)\n"
+ "PrintOnNextLine( 0.3s | command_2)\n"
+ "MoveUp(2)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ // Check that if not enough time passes by, no update is performed.
+ clock.AdvanceTimeMillis(50); // time == 620
+ table.UpdateTable();
+ ASSERT_EQ(table.log(), "");
+
+ clock.AdvanceTimeMillis(50); // time == 670
+ table.UpdateTable();
+ ASSERT_EQ(table.log(),
+ "PrintOnNextLine( 0.6s | command_1)\n"
+ "PrintOnNextLine( 0.4s | command_2)\n"
+ "MoveUp(2)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ table.log_.clear();
+ clock.AdvanceTimeMillis(100); // time == 770
+ table.UpdateTable();
+ ASSERT_EQ(table.log(),
+ "PrintOnNextLine( 0.7s | command_1)\n"
+ "PrintOnNextLine( 0.5s | command_2)\n"
+ "MoveUp(2)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ table.CommandEnded(cmd1);
+ clock.AdvanceTimeMillis(100); // time == 870
+ table.UpdateTable();
+ ASSERT_EQ(table.log(),
+ "PrintOnNextLine( 0.6s | command_2)\n"
+ "PrintOnNextLine( 0.3s | command_3)\n"
+ "MoveUp(2)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ clock.AdvanceTimeMillis(400); // time == 1270
+ table.CommandEnded(cmd2);
+ table.UpdateTable();
+
+ ASSERT_EQ(table.log(),
+ "PrintOnNextLine( 0.7s | command_3)\n"
+ "ClearNextLine()\n"
+ "MoveUp(2)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ table.CommandEnded(cmd3);
+ clock.AdvanceTimeMillis(100); // time == 1370
+ table.UpdateTable();
+ ASSERT_EQ(table.log(),
+ "ClearNextLine()\n"
+ "MoveUp(1)\n"
+ "PrintOnCurrentLine(some_status)\n"
+ "Flush()\n");
+
+ table.BuildEnded();
+ table.UpdateTable();
+ ASSERT_EQ(table.log_, "Flush()\n");
+}