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");
+}