Repository: doctest/doctest (language: cpp)

Issue:
bug: doctest::String::substr() off by one at the end
## Description

`doctest::String::substr()` cuts short the last character, when extracting from the end of the `String`.


### Steps to reproduce

```
#include <doctest/doctest.h>

TEST_SUITE("doctest unit tests") {
    TEST_CASE("doctest::String::substr()") {
        const doctest::String abcde = "abcde";
        CHECK(abcde.substr(0, 3) == "abc");
        CHECK(abcde.substr(2, 3) == "cde");
        CHECK(abcde.substr(0, 5) == "abcde");
    }
}
```

```
===============================================================================
check_doctest_string.cpp(0):
TEST SUITE: doctest unit tests
TEST CASE:  doctest::String::substr()

check_doctest_string.cpp(0): ERROR: CHECK( abcde.substr(2, 3) == "cde" ) is NOT correct!
  values: CHECK( cd == cde )

check_doctest_string.cpp(0): ERROR: CHECK( abcde.substr(0, 5) == "abcde" ) is NOT correct!
  values: CHECK( abcd == abcde )

===============================================================================
```


### Extra information

* doctest version: **v2.4.9** ... v2.4.11
* Operating System: **22.04.1-Ubuntu**
* Compiler+version: **GCC 12.3.0 x86_64**



Discussion / hints:

Fix patch:
diff --git a/examples/all_features/CMakeLists.txt b/examples/all_features/CMakeLists.txt
index 19436c549..7b6277007 100644
--- a/examples/all_features/CMakeLists.txt
+++ b/examples/all_features/CMakeLists.txt
@@ -9,6 +9,7 @@ set(files_with_output
     alternative_macros.cpp
     assertion_macros.cpp
     stringification.cpp
+    check_doctest_string.cpp
     double_stringification.cpp
     reporters_and_listeners.cpp
     subcases.cpp


Test patch:
diff --git a/doctest/doctest.h b/doctest/doctest.h
index 5c754cde0..7df38834d 100644
--- a/doctest/doctest.h
+++ b/doctest/doctest.h
@@ -3751,7 +3751,7 @@ String::size_type String::capacity() const {
 }
 
 String String::substr(size_type pos, size_type cnt) && {
-    cnt = std::min(cnt, size() - 1 - pos);
+    cnt = std::min(cnt, size() - pos);
     char* cptr = c_str();
     memmove(cptr, cptr + pos, cnt);
     setSize(cnt);
@@ -3759,7 +3759,7 @@ String String::substr(size_type pos, size_type cnt) && {
 }
 
 String String::substr(size_type pos, size_type cnt) const & {
-    cnt = std::min(cnt, size() - 1 - pos);
+    cnt = std::min(cnt, size() - pos);
     return String{ c_str() + pos, cnt };
 }
 
diff --git a/doctest/parts/doctest.cpp b/doctest/parts/doctest.cpp
index 5a2dd4599..19ba25a4f 100644
--- a/doctest/parts/doctest.cpp
+++ b/doctest/parts/doctest.cpp
@@ -661,7 +661,7 @@ String::size_type String::capacity() const {
 }
 
 String String::substr(size_type pos, size_type cnt) && {
-    cnt = std::min(cnt, size() - 1 - pos);
+    cnt = std::min(cnt, size() - pos);
     char* cptr = c_str();
     memmove(cptr, cptr + pos, cnt);
     setSize(cnt);
@@ -669,7 +669,7 @@ String String::substr(size_type pos, size_type cnt) && {
 }
 
 String String::substr(size_type pos, size_type cnt) const & {
-    cnt = std::min(cnt, size() - 1 - pos);
+    cnt = std::min(cnt, size() - pos);
     return String{ c_str() + pos, cnt };
 }
 
diff --git a/examples/all_features/check_doctest_string.cpp b/examples/all_features/check_doctest_string.cpp
new file mode 100644
index 000000000..2fce03506
--- /dev/null
+++ b/examples/all_features/check_doctest_string.cpp
@@ -0,0 +1,10 @@
+#include <doctest/doctest.h>
+
+TEST_SUITE("doctest unit tests") {
+    TEST_CASE("doctest::String::substr()") {
+        const doctest::String abcde = "abcde";
+        CHECK(abcde.substr(0, 3) == "abc");
+        CHECK(abcde.substr(2, 3) == "cde");
+        CHECK(abcde.substr(0, 5) == "abcde");
+    }
+}
\ No newline at end of file
diff --git a/examples/all_features/test_output/check_doctest_string.cpp.txt b/examples/all_features/test_output/check_doctest_string.cpp.txt
new file mode 100644
index 000000000..75be56a8f
--- /dev/null
+++ b/examples/all_features/test_output/check_doctest_string.cpp.txt
@@ -0,0 +1,6 @@
+[doctest] run with "--help" for options
+===============================================================================
+[doctest] test cases: 1 | 1 passed | 0 failed |
+[doctest] assertions: 3 | 3 passed | 0 failed |
+[doctest] Status: SUCCESS!
+Program code.
diff --git a/examples/all_features/test_output/check_doctest_string.cpp_junit.txt b/examples/all_features/test_output/check_doctest_string.cpp_junit.txt
new file mode 100644
index 000000000..eb141a57b
--- /dev/null
+++ b/examples/all_features/test_output/check_doctest_string.cpp_junit.txt
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites>
+  <testsuite name="all_features" errors="0" failures="0" tests="3">
+    <testcase classname="check_doctest_string.cpp" name="doctest::String::substr()" status="run"/>
+  </testsuite>
+</testsuites>
+Program code.
diff --git a/examples/all_features/test_output/check_doctest_string.cpp_xml.txt b/examples/all_features/test_output/check_doctest_string.cpp_xml.txt
new file mode 100644
index 000000000..12c39533d
--- /dev/null
+++ b/examples/all_features/test_output/check_doctest_string.cpp_xml.txt
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<doctest binary="all_features">
+  <Options order_by="file" rand_seed="324" first="0" last="4294967295" abort_after="0" subcase_filter_levels="2147483647" case_sensitive="false" no_throw="false" no_skip="false"/>
+  <TestSuite name="doctest unit tests">
+    <TestCase name="doctest::String::substr()" filename="check_doctest_string.cpp" line="0">
+      <OverallResultsAsserts successes="3" failures="0" test_case_success="true"/>
+    </TestCase>
+  </TestSuite>
+  <OverallResultsAsserts successes="3" failures="0"/>
+  <OverallResultsTestCases successes="1" failures="0"/>
+</doctest>
+Program code.
diff --git a/examples/all_features/test_output/filter_2.txt b/examples/all_features/test_output/filter_2.txt
index 7d43c001c..cc3557f98 100644
--- a/examples/all_features/test_output/filter_2.txt
+++ b/examples/all_features/test_output/filter_2.txt
@@ -1,6 +1,6 @@
 [doctest] run with "--help" for options
 ===============================================================================
-[doctest] test cases: 0 | 0 passed | 0 failed | 108 skipped
+[doctest] test cases: 0 | 0 passed | 0 failed | 109 skipped
 [doctest] assertions: 0 | 0 passed | 0 failed |
 [doctest] Status: SUCCESS!
 Program code.
diff --git a/examples/all_features/test_output/filter_2_xml.txt b/examples/all_features/test_output/filter_2_xml.txt
index f1c124dac..7905baa89 100644
--- a/examples/all_features/test_output/filter_2_xml.txt
+++ b/examples/all_features/test_output/filter_2_xml.txt
@@ -42,6 +42,9 @@
     <TestCase name="default construction&lt;signed char>" filename="templated_test_cases.cpp" line="0" skipped="true"/>
     <TestCase name="default construction&lt;unsigned char>" filename="templated_test_cases.cpp" line="0" skipped="true"/>
   </TestSuite>
+  <TestSuite name="doctest unit tests">
+    <TestCase name="doctest::String::substr()" filename="check_doctest_string.cpp" line="0" skipped="true"/>
+  </TestSuite>
   <TestSuite name="test suite with a description">
     <TestCase name="doesn't fail but it should have" filename="test_cases_and_suites.cpp" line="0" description="regarding failures" should_fail="true" skipped="true"/>
     <TestCase name="doesn't fail which is fine" filename="test_cases_and_suites.cpp" line="0" description="regarding failures" may_fail="true" skipped="true"/>
@@ -152,6 +155,6 @@
     <TestCase name="without a funny name:" filename="subcases.cpp" line="0" skipped="true"/>
   </TestSuite>
   <OverallResultsAsserts successes="0" failures="0"/>
-  <OverallResultsTestCases successes="0" failures="0" skipped="108"/>
+  <OverallResultsTestCases successes="0" failures="0" skipped="109"/>
 </doctest>
 Program code.
diff --git a/examples/all_features/test_output/no_multi_lane_atomics.txt b/examples/all_features/test_output/no_multi_lane_atomics.txt
index 4469b9154..15c128634 100644
--- a/examples/all_features/test_output/no_multi_lane_atomics.txt
+++ b/examples/all_features/test_output/no_multi_lane_atomics.txt
@@ -925,7 +925,7 @@ TEST CASE:  without a funny name:
 subcases.cpp(0): MESSAGE: Nooo
 
 ===============================================================================
-[doctest] test cases:  86 |  35 passed |  51 failed |
-[doctest] assertions: 235 | 115 passed | 120 failed |
+[doctest] test cases:  87 |  36 passed |  51 failed |
+[doctest] assertions: 238 | 118 passed | 120 failed |
 [doctest] Status: FAILURE!
 Program code.
diff --git a/examples/all_features/test_output/no_multithreading.txt b/examples/all_features/test_output/no_multithreading.txt
index 4469b9154..15c128634 100644
--- a/examples/all_features/test_output/no_multithreading.txt
+++ b/examples/all_features/test_output/no_multithreading.txt
@@ -925,7 +925,7 @@ TEST CASE:  without a funny name:
 subcases.cpp(0): MESSAGE: Nooo
 
 ===============================================================================
-[doctest] test cases:  86 |  35 passed |  51 failed |
-[doctest] assertions: 235 | 115 passed | 120 failed |
+[doctest] test cases:  87 |  36 passed |  51 failed |
+[doctest] assertions: 238 | 118 passed | 120 failed |
 [doctest] Status: FAILURE!
 Program code.
diff --git a/examples/all_features/test_output/std_headers.txt b/examples/all_features/test_output/std_headers.txt
index 4469b9154..15c128634 100644
--- a/examples/all_features/test_output/std_headers.txt
+++ b/examples/all_features/test_output/std_headers.txt
@@ -925,7 +925,7 @@ TEST CASE:  without a funny name:
 subcases.cpp(0): MESSAGE: Nooo
 
 ===============================================================================
-[doctest] test cases:  86 |  35 passed |  51 failed |
-[doctest] assertions: 235 | 115 passed | 120 failed |
+[doctest] test cases:  87 |  36 passed |  51 failed |
+[doctest] assertions: 238 | 118 passed | 120 failed |
 [doctest] Status: FAILURE!
 Program code.