From 9239e5ee07e3ca87a9731364d29555efa16fcd63 Mon Sep 17 00:00:00 2001
From: Nick Draper <nick.draper@stfc.ac.uk>
Date: Tue, 4 Jul 2017 15:55:28 +0100
Subject: [PATCH] Do not alter property.value, just add valuePrettyPrint

and make use in history and logging

re #15164
---
 .../API/inc/MantidAPI/AlgorithmHistory.h      |  2 +-
 Framework/API/src/Algorithm.cpp               | 10 ++-
 Framework/API/src/AlgorithmHistory.cpp        |  8 +-
 Framework/Kernel/inc/MantidKernel/Property.h  |  5 +-
 .../Kernel/inc/MantidKernel/PropertyHelper.h  | 77 ++++++++++++++-----
 .../Kernel/inc/MantidKernel/PropertyHistory.h |  2 +-
 .../inc/MantidKernel/PropertyWithValue.h      |  2 +
 .../inc/MantidKernel/PropertyWithValue.tcc    |  8 ++
 Framework/Kernel/inc/MantidKernel/Strings.h   |  3 +
 Framework/Kernel/src/Property.cpp             | 13 ++++
 Framework/Kernel/src/PropertyHistory.cpp      | 23 +++---
 Framework/Kernel/src/Strings.cpp              | 25 ++++++
 Framework/Kernel/test/ArrayPropertyTest.h     | 11 +--
 Framework/Kernel/test/PropertyHistoryTest.h   | 15 ++++
 Framework/Kernel/test/StringsTest.h           | 63 +++++++++++++++
 15 files changed, 227 insertions(+), 40 deletions(-)

diff --git a/Framework/API/inc/MantidAPI/AlgorithmHistory.h b/Framework/API/inc/MantidAPI/AlgorithmHistory.h
index 5ca1739a633..1466bbad3d0 100644
--- a/Framework/API/inc/MantidAPI/AlgorithmHistory.h
+++ b/Framework/API/inc/MantidAPI/AlgorithmHistory.h
@@ -121,7 +121,7 @@ public:
   /// Retrieve the number of child algorithms
   size_t childHistorySize() const;
   /// print contents of object
-  void printSelf(std::ostream &, const int indent = 0) const;
+  void printSelf(std::ostream &, const int indent = 0, const size_t maxPropertyLength = 0) const;
   /// Less than operator
   inline bool operator<(const AlgorithmHistory &other) const {
     return (execCount() < other.execCount());
diff --git a/Framework/API/src/Algorithm.cpp b/Framework/API/src/Algorithm.cpp
index 92fe324c22e..2341774faea 100644
--- a/Framework/API/src/Algorithm.cpp
+++ b/Framework/API/src/Algorithm.cpp
@@ -1059,8 +1059,14 @@ void Algorithm::logAlgorithmInfo() const {
     logger.notice() << '\n';
     // Make use of the AlgorithmHistory class, which holds all the info we want
     // here
-    AlgorithmHistory AH(this);
-    logger.information() << AH;
+    AlgorithmHistory algHistory(this);
+    size_t maxPropertyLength = 40;
+    if (logger.is(Logger::Priority::PRIO_DEBUG))
+    {
+      //include the full property value when logging in debug
+      maxPropertyLength = 0;
+    }
+    algHistory.printSelf(logger.information(), 0, maxPropertyLength);
   }
 }
 
diff --git a/Framework/API/src/AlgorithmHistory.cpp b/Framework/API/src/AlgorithmHistory.cpp
index cc9c2475ba6..1d4d56d51a5 100644
--- a/Framework/API/src/AlgorithmHistory.cpp
+++ b/Framework/API/src/AlgorithmHistory.cpp
@@ -201,11 +201,13 @@ AlgorithmHistory::getChildAlgorithm(const size_t index) const {
 }
 
 /** Prints a text representation of itself
- *  @param os :: The ouput stream to write to
+ *  @param os :: The output stream to write to
  *  @param indent :: an indentation value to make pretty printing of object and
  * sub-objects
+ *  @param maxPropertyLength :: the max length for any property value string (0 = full length)
  */
-void AlgorithmHistory::printSelf(std::ostream &os, const int indent) const {
+void AlgorithmHistory::printSelf(std::ostream &os, const int indent,
+  const size_t maxPropertyLength) const {
   os << std::string(indent, ' ') << "Algorithm: " << m_name;
   os << std::string(indent, ' ') << " v" << m_version << '\n';
 
@@ -217,7 +219,7 @@ void AlgorithmHistory::printSelf(std::ostream &os, const int indent) const {
   os << std::string(indent, ' ') << "Parameters:\n";
 
   for (const auto &property : m_properties) {
-    property->printSelf(os, indent + 2);
+    property->printSelf(os, indent + 2, maxPropertyLength);
   }
 }
 
diff --git a/Framework/Kernel/inc/MantidKernel/Property.h b/Framework/Kernel/inc/MantidKernel/Property.h
index 5179ef8fe0b..f6c1eaef2c4 100644
--- a/Framework/Kernel/inc/MantidKernel/Property.h
+++ b/Framework/Kernel/inc/MantidKernel/Property.h
@@ -140,7 +140,10 @@ public:
                                 "', property type not implemented.");
   }
   /// Returns the value of the property as a string
-  virtual std::string value() const = 0;
+  virtual std::string value() const = 0;  
+  /// Returns the value of the property as a pretty printed string
+  virtual std::string valuePrettyPrint(size_t maxLength = 0,
+    bool collapseLists = true) const;
   /// Set the value of the property via a string.  If the value is unacceptable
   /// the value is not changed but a string is returned
   virtual std::string setValue(const std::string &) = 0;
diff --git a/Framework/Kernel/inc/MantidKernel/PropertyHelper.h b/Framework/Kernel/inc/MantidKernel/PropertyHelper.h
index d8398ad9c93..5fb3fb4e241 100644
--- a/Framework/Kernel/inc/MantidKernel/PropertyHelper.h
+++ b/Framework/Kernel/inc/MantidKernel/PropertyHelper.h
@@ -28,13 +28,63 @@ template <typename T> std::string toString(const boost::shared_ptr<T> &value) {
   throw boost::bad_lexical_cast();
 }
 
+/// Specialization for a property of type std::vector.
+template <typename T>
+std::string toString(const std::vector<T> &value,
+  const std::string &delimiter = ",") {
+  std::stringstream result;
+  std::size_t vsize = value.size();
+  for (std::size_t i = 0; i < vsize; ++i) {
+    result << value[i];
+    if (i + 1 != vsize)
+      result << delimiter;
+  }
+  return result.str();
+}
+
+/// Specialization for a property of type std::vector<std::vector>.
+template <typename T>
+std::string toString(const std::vector<std::vector<T>> &value,
+  const std::string &outerDelimiter = ",",
+  const std::string &innerDelimiter = "+") {
+  std::stringstream result;
+  std::size_t vsize = value.size();
+  for (std::size_t i = 0; i < vsize; ++i) {
+    std::size_t innervsize = value[i].size();
+    for (std::size_t j = 0; j < innervsize; ++j) {
+      result << value[i][j];
+      if (j + 1 != innervsize)
+        result << innerDelimiter;
+    }
+
+    if (i + 1 != vsize)
+      result << outerDelimiter;
+  }
+  return result.str();
+}
+
+// --------------------- convert values to pretty strings
+/// Convert values to pretty strings.
+template <typename T> std::string toPrettyString(const T &value, size_t maxLength = 0,
+  bool collapseLists = true) {
+  return boost::lexical_cast<std::string>(value);
+}
+
+/// Throw an exception if a shared pointer is converted to a pretty string.
+template <typename T> std::string toPrettyString(const boost::shared_ptr<T> &value, size_t maxLength = 0,
+  bool collapseLists = true) {
+  UNUSED_ARG(value);
+  throw boost::bad_lexical_cast();
+}
+
 /** Specialization for a property of type std::vector of non integral types.
 *   This will catch Vectors of char, double, float etc.
 *   This simply concatenates the values using a delimiter
 */
 template <typename T>
 std::string
-toString(const std::vector<T> &value, const std::string &delimiter = ",",
+toPrettyString(const std::vector<T> &value, size_t maxLength = 0,
+  bool collapseLists = true, const std::string &delimiter = ",",
          const std::string &unusedDelimiter = "+",
          typename std::enable_if<!(std::is_integral<T>::value &&
                                    std::is_arithmetic<T>::value)>::type * = 0) {
@@ -51,7 +101,8 @@ toString(const std::vector<T> &value, const std::string &delimiter = ",",
 */
 template <typename T>
 std::string
-toString(const std::vector<T> &value, const std::string &delimiter = ",",
+toPrettyString(const std::vector<T> &value, size_t maxLength = 0,
+  bool collapseLists = true, const std::string &delimiter = ",",
          const std::string &listDelimiter = "-",
          typename std::enable_if<std::is_integral<T>::value &&
                                  std::is_arithmetic<T>::value>::type * = 0) {
@@ -65,7 +116,8 @@ toString(const std::vector<T> &value, const std::string &delimiter = ",",
 */
 template <>
 std::string
-toString(const std::vector<bool> &value, const std::string &delimiter,
+toPrettyString(const std::vector<bool> &value, size_t maxLength,
+         bool collapseLists, const std::string &delimiter,
          const std::string &unusedDelimiter,
          typename std::enable_if<std::is_same<bool, bool>::value>::type *) {
   UNUSED_ARG(unusedDelimiter);
@@ -74,23 +126,12 @@ toString(const std::vector<bool> &value, const std::string &delimiter,
 
 /// Specialization for a property of type std::vector<std::vector>.
 template <typename T>
-std::string toString(const std::vector<std::vector<T>> &value,
+std::string toPrettyString(const std::vector<std::vector<T>> &value, 
+                     size_t maxLength = 0,
+                     bool collapseLists = true,
                      const std::string &outerDelimiter = ",",
                      const std::string &innerDelimiter = "+") {
-  std::stringstream result;
-  std::size_t vsize = value.size();
-  for (std::size_t i = 0; i < vsize; ++i) {
-    std::size_t innervsize = value[i].size();
-    for (std::size_t j = 0; j < innervsize; ++j) {
-      result << value[i][j];
-      if (j + 1 != innervsize)
-        result << innerDelimiter;
-    }
-
-    if (i + 1 != vsize)
-      result << outerDelimiter;
-  }
-  return result.str();
+  return toString<T>(value, outerDelimiter, innerDelimiter);
 }
 
 /// Specialization for any type, should be appropriate for properties with a
diff --git a/Framework/Kernel/inc/MantidKernel/PropertyHistory.h b/Framework/Kernel/inc/MantidKernel/PropertyHistory.h
index 822073a13c5..4f300cce3ac 100644
--- a/Framework/Kernel/inc/MantidKernel/PropertyHistory.h
+++ b/Framework/Kernel/inc/MantidKernel/PropertyHistory.h
@@ -70,7 +70,7 @@ public:
   /// get direction flag of algorithm parameter const
   unsigned int direction() const { return m_direction; };
   /// print contents of object
-  void printSelf(std::ostream &, const int indent = 0) const;
+  void printSelf(std::ostream &, const int indent = 0, const size_t maxPropertyLength = 0) const;
   /// get whether algorithm parameter was left as default EMPTY_INT,LONG,DBL
   /// const
   bool isEmptyDefault() const;
diff --git a/Framework/Kernel/inc/MantidKernel/PropertyWithValue.h b/Framework/Kernel/inc/MantidKernel/PropertyWithValue.h
index fab07ec92ba..d6f8684c2dd 100644
--- a/Framework/Kernel/inc/MantidKernel/PropertyWithValue.h
+++ b/Framework/Kernel/inc/MantidKernel/PropertyWithValue.h
@@ -66,6 +66,8 @@ public:
 
   void saveProperty(::NeXus::File *file) override;
   std::string value() const override;
+  std::string valuePrettyPrint(size_t maxLength = 0,
+    bool collapseLists = true) const;
   virtual bool operator==(const PropertyWithValue<TYPE> &rhs) const;
   virtual bool operator!=(const PropertyWithValue<TYPE> &rhs) const;
   int size() const override;
diff --git a/Framework/Kernel/inc/MantidKernel/PropertyWithValue.tcc b/Framework/Kernel/inc/MantidKernel/PropertyWithValue.tcc
index fb0bdb27655..bd6bc0a5730 100644
--- a/Framework/Kernel/inc/MantidKernel/PropertyWithValue.tcc
+++ b/Framework/Kernel/inc/MantidKernel/PropertyWithValue.tcc
@@ -114,6 +114,14 @@ template <typename TYPE> std::string PropertyWithValue<TYPE>::value() const {
   return toString(m_value);
 }
 
+/** Get the value of the property as a string
+ *  @return The property's value
+ */
+template <typename TYPE> std::string PropertyWithValue<TYPE>::valuePrettyPrint(size_t maxLength,
+    bool collapseLists) const const {
+  return toPrettyString(m_value, maxLength, collapseLists);
+}
+
 /**
  * Deep comparison.
  * @param rhs The other property to compare to.
diff --git a/Framework/Kernel/inc/MantidKernel/Strings.h b/Framework/Kernel/inc/MantidKernel/Strings.h
index 086d992cce4..4cc570346d6 100644
--- a/Framework/Kernel/inc/MantidKernel/Strings.h
+++ b/Framework/Kernel/inc/MantidKernel/Strings.h
@@ -129,6 +129,9 @@ DLLExport std::string joinCompress(ITERATOR_TYPE begin, ITERATOR_TYPE end,
   }
   return result.str();
 }
+/// Return a string with all matching occurence-strings
+MANTID_KERNEL_DLL std::string shortenList(const std::string &input,
+  const size_t &max_length);
 
 /// Return a string with all matching occurence-strings
 MANTID_KERNEL_DLL std::string replace(const std::string &input,
diff --git a/Framework/Kernel/src/Property.cpp b/Framework/Kernel/src/Property.cpp
index c64aa7ae412..bdd8b903835 100644
--- a/Framework/Kernel/src/Property.cpp
+++ b/Framework/Kernel/src/Property.cpp
@@ -4,6 +4,7 @@
 #include "MantidKernel/Exception.h"
 #include "MantidKernel/IPropertySettings.h"
 #include "MantidKernel/PropertyHistory.h"
+#include "MantidKernel/Strings.h"
 #include "MantidKernel/TimeSeriesProperty.h"
 
 #include <unordered_map>
@@ -111,6 +112,18 @@ bool Property::remember() const { return m_remember; }
  */
 void Property::setRemember(bool remember) { m_remember = remember; }
 
+/**
+* Returns the value as a pretty printed string
+* The default implementation just returns the value with the size limit applied
+* @param maxLength :: The Max length of the returned string
+* @param collapseLists :: Whether to collapse 1,2,3 into 1-3
+*/
+std::string Property::valuePrettyPrint(size_t maxLength,
+  bool collapseLists) const {
+  UNUSED_ARG(collapseLists);
+  return Strings::shortenList(value(),maxLength);
+}
+
 /** Sets the user level description of the property.
  *  In addition, if the brief documentation string is empty it will be set to
  *  the portion of the provided string up to the first period
diff --git a/Framework/Kernel/src/PropertyHistory.cpp b/Framework/Kernel/src/PropertyHistory.cpp
index d63b06066e9..916eca6810f 100644
--- a/Framework/Kernel/src/PropertyHistory.cpp
+++ b/Framework/Kernel/src/PropertyHistory.cpp
@@ -4,6 +4,7 @@
 #include "MantidKernel/EmptyValues.h"
 #include "MantidKernel/PropertyHistory.h"
 #include "MantidKernel/Property.h"
+#include "MantidKernel/Strings.h"
 
 #include <boost/lexical_cast.hpp>
 #include <algorithm>
@@ -22,28 +23,32 @@ PropertyHistory::PropertyHistory(const std::string &name,
       m_direction(direction) {}
 
 PropertyHistory::PropertyHistory(Property const *const prop)
-    : // PropertyHistory::PropertyHistory(prop->name(), prop->value(),
-      // prop->type(), prop->isDefault(), prop->direction())
-      m_name(prop->name()),
-      m_value(prop->value()), m_type(prop->type()),
+    : m_name(prop->name()),
+      m_value(prop->valuePrettyPrint(0,true)), m_type(prop->type()),
       m_isDefault(prop->isDefault()), m_direction(prop->direction()) {}
 
 /** Prints a text representation of itself
- *  @param os :: The ouput stream to write to
+ *  @param os :: The output stream to write to
  *  @param indent :: an indentation value to make pretty printing of object and
  * sub-objects
+ *  @param maxPropertyLength :: the max length for any property value string (0 = full length)
  */
-void PropertyHistory::printSelf(std::ostream &os, const int indent) const {
+void PropertyHistory::printSelf(std::ostream &os, const int indent,
+  const size_t maxPropertyLength) const {
   os << std::string(indent, ' ') << "Name: " << m_name;
-  os << ", Value: " << m_value;
+  if ((maxPropertyLength > 0) && (m_value.size() > maxPropertyLength)) {
+    os << ", Value: " << Strings::shortenList(m_value, maxPropertyLength);
+  } else {
+    os << ", Value: " << m_value;
+  }
   os << ", Default?: " << (m_isDefault ? "Yes" : "No");
   os << ", Direction: " << Kernel::Direction::asText(m_direction) << '\n';
 }
 
 /** Prints a text representation
- *  @param os :: The ouput stream to write to
+ *  @param os :: The output stream to write to
  *  @param AP :: The PropertyHistory to output
- *  @returns The ouput stream
+ *  @returns The output stream
  */
 std::ostream &operator<<(std::ostream &os, const PropertyHistory &AP) {
   AP.printSelf(os);
diff --git a/Framework/Kernel/src/Strings.cpp b/Framework/Kernel/src/Strings.cpp
index 80557bc268d..d304bea3862 100644
--- a/Framework/Kernel/src/Strings.cpp
+++ b/Framework/Kernel/src/Strings.cpp
@@ -37,6 +37,31 @@ std::string loadFile(const std::string &filename) {
   return retVal;
 }
 
+// ------------------------------------------------------------------------------------------------
+/** Return a string shortened with the center replace by " ... "
+* If the string is already short enough then the original string will be returned.
+* If the max length or input string length is smaller than the ellipsis, the input will be returned.
+*
+* @param input :: input string
+* @param max_length :: The maximum length of the return string (0 = return full string)
+* @return the modified string.
+*/
+std::string shortenList(const std::string & input, const size_t & max_length) {
+  const std::string ellipsis = " ... ";
+  const size_t ellipsisSize = ellipsis.size();
+  // limit too small or input too small, return input string
+  if ((max_length == 0) || 
+      (input.size() < ellipsisSize+2) || 
+      (input.size() <= max_length))
+    return input;
+
+  const size_t end_length = (max_length - ellipsisSize) / 2;
+  std::string retVal = input.substr(0, end_length) +
+    ellipsis + 
+    input.substr(input.size() - end_length, end_length);
+  return retVal;
+}
+
 //------------------------------------------------------------------------------------------------
 /** Return a string with all matching occurence-strings
  *
diff --git a/Framework/Kernel/test/ArrayPropertyTest.h b/Framework/Kernel/test/ArrayPropertyTest.h
index 7a14e268c7a..4914ee29058 100644
--- a/Framework/Kernel/test/ArrayPropertyTest.h
+++ b/Framework/Kernel/test/ArrayPropertyTest.h
@@ -82,7 +82,7 @@ public:
     TS_ASSERT_EQUALS(i.operator()()[0], 1);
     TS_ASSERT_EQUALS(i.operator()()[1], 2);
     TS_ASSERT_EQUALS(i.operator()()[2], 3);
-    TS_ASSERT_EQUALS(i.getDefault(), "1-3");
+    TS_ASSERT_EQUALS(i.getDefault(), "1,2,3");
     TS_ASSERT(i.isDefault());
 
     ArrayProperty<int> i2("i", "-1-1");
@@ -301,7 +301,7 @@ public:
                       static_cast<Property *>(0))
   }
 
-  void testListShortening() {
+  void testPrettyPrinting() {
     std::vector<std::string> inputList{
         "1,2,3", "-1,0,1", "356,366,367,368,370,371,372,375", "7,6,5,6,7,8,10",
         "1-9998, 9999, 2000, 20002-29999"};
@@ -352,9 +352,10 @@ public:
 
     bool success = true;
     for (size_t i = 0; i < inputList.size(); i++) {
-      ArrayProperty<T> intListProperty("i", inputList[i]);
-      TS_ASSERT_EQUALS(intListProperty.value(), resultList[i]);
-      if (intListProperty.value() != resultList[i]) {
+      ArrayProperty<T> listProperty("i", inputList[i]);
+      std::string response = listProperty.valuePrettyPrint(0,true);
+      TS_ASSERT_EQUALS(response, resultList[i]);
+      if (response != resultList[i]) {
         success = false;
       }
     }
diff --git a/Framework/Kernel/test/PropertyHistoryTest.h b/Framework/Kernel/test/PropertyHistoryTest.h
index fa09873b510..519c592fd4c 100644
--- a/Framework/Kernel/test/PropertyHistoryTest.h
+++ b/Framework/Kernel/test/PropertyHistoryTest.h
@@ -28,6 +28,21 @@ public:
     TS_ASSERT_EQUALS(output.str(), correctOutput);
   }
 
+  void testOutputWithShortenedValue() {
+    std::string correctOutput = "Name: arg1_param, ";
+    correctOutput = correctOutput + "Value: 1234567 ... 4567890, ";
+    correctOutput = correctOutput + "Default?: Yes, ";
+    correctOutput = correctOutput + "Direction: Input\n";
+
+    // a long property that should get shortened
+    PropertyHistory propHistory("arg1_param", "123456798012345678901234567890", "argument", true, Direction::Input);
+
+    // dump output to sting
+    std::ostringstream output;
+    TS_ASSERT_THROWS_NOTHING(propHistory.printSelf(output,0,20));
+    TS_ASSERT_EQUALS(output.str(), correctOutput);
+  }
+
   /**
    * Test the isEmptyDefault method returns true for unset default-value
    * properties
diff --git a/Framework/Kernel/test/StringsTest.h b/Framework/Kernel/test/StringsTest.h
index f6429f13cd1..4e42b24cab8 100644
--- a/Framework/Kernel/test/StringsTest.h
+++ b/Framework/Kernel/test/StringsTest.h
@@ -285,6 +285,69 @@ public:
     }
   }
 
+  void test_shortenList() {
+
+    std::vector<std::string> inputList{
+      "", // empty
+      "1,2", // shorter than the ellipsis
+      "1,2,3", // equal in length than the ellipsis
+      "1,2,35", // one longer than the ellipsis
+      "1,2,3,4", // two longer than the ellipsis
+      "1,2,3,45", // just long enough for the ellipsis
+      "12,3,4,56", // another past the ellipsis
+      "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20" };
+    std::vector<std::string> resultListMaxLength7{
+      "", // empty
+      "1,2", // shorter than the ellipsis
+      "1,2,3", // equal in length than the ellipsis
+      "1,2,35", // one longer than the ellipsis
+      "1,2,3,4", // two longer than the ellipsis
+      "1 ... 5", // just long enough for the ellipsis
+      "1 ... 6", // another past the ellipsis
+      "1 ... 0" };
+
+    std::vector<std::string> resultListMaxLength20{
+      "", // empty
+      "1,2", // shorter than the ellipsis
+      "1,2,3", // equal in length than the ellipsis
+      "1,2,35", // one longer than the ellipsis
+      "1,2,3,4", // two longer than the ellipsis
+      "1,2,3,45", // just long enough for the ellipsis
+      "12,3,4,56", // another past the ellipsis
+      "1,2,3,4 ... 8,19,20" };
+
+    //test very short max size
+    int maxLength = 7;
+    for (size_t i = 0; i < inputList.size(); i++) {
+      const auto &input = inputList[i];
+      std::string result = shortenList(input, maxLength);
+      TS_ASSERT_EQUALS(
+        result,
+        resultListMaxLength7[i]);
+      TS_ASSERT_LESS_THAN_EQUALS(
+        result.size(),
+        maxLength);
+      TS_ASSERT_LESS_THAN_EQUALS(
+        result.size(),
+        input.size());
+    }
+    //test longer max size
+    maxLength = 20;
+    for (size_t i = 0; i < inputList.size(); i++) {
+      const auto &input = inputList[i];
+      std::string result = shortenList(input, maxLength);
+      TS_ASSERT_EQUALS(
+        result,
+        resultListMaxLength20[i]);
+      TS_ASSERT_LESS_THAN_EQUALS(
+        result.size(),
+        maxLength);      
+      TS_ASSERT_LESS_THAN_EQUALS(
+          result.size(),
+          input.size());
+    }
+  }
+
   void test_endsWithInt() {
     TS_ASSERT_EQUALS(endsWithInt("pixel22"), 22);
     TS_ASSERT_EQUALS(endsWithInt("pixel000123"), 123);
-- 
GitLab