Repository: robotframework/robotframework

Issue:
All iterations of templated tests should be executed even if one is skipped
In the documentation, it is written that :

> Templated tests are also special so that all the rounds are executed even if one or more of them fails. It is possible to use this kind of [continue on failure](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#continue-on-failure) mode with normal tests too, but with the templated tests the mode is on automatically.

In particular : `all the rounds are executed even if one or more of them fails`
However, if one is skipped, none the following rounds are not executed.
I don't know if it was intended or not but it caused some problems on my tests.

Example:
```
*** Test Cases ***
Eat yellow fruits
    [Documentation]    Eat only yellow fruits, Fails if item given is a fruit which is not yellow and skip if item is not a fruit
    [Template]    Check fruit and eat it
    red_apple       # FAIL
    banana          # PASS
    plane           # SKIP => not a fruit
    lemon           # should be PASS but is not executed 
```

Discussion:
@pekkaklarck: The documentation is from the time when the SKIP status didn't exist. I believe we didn't really think about this when implementing SKIP, but I have a vague feeling this has been discussed at least once afterwards. I kind of agree it would make sense to run all iterations in this case, but then again explicitly skipping an iteration can be considered to mean "skip remaining iterations".

If we agree to run all iterations also after SKIP, we need to decide what status the test should have if some iterations FAIL. Nowadays an iteration ending with SKIP after an iteration with FAIL gives the test the SKIP status. I believe FAIL after SKIP should behave the same way, but we could also consider changing the overall logic so that any failed iteration gives the test the FAIL status. That would match how any failed test gives a suite the FAIL status regardless of the status of the other tests. Then again, SKIP in test teardown overrides possible earlier FAIL.

I think it would be good to hear opinions from others regarding to these semantics. I'll start a thread about this on the #robotframework channel on our Slack.@pekkaklarck: If we'd change semantics so that tests get the FAIL status if any iteration fails, regardless are some iterations skipped or not, we should also decided how to handle a case where some iterations pass and some are skipped. I think logically then the test status should be PASS. The test would then get the SKIP status only if all iterations are skipped. This is the same logic we use when deciding the suite status.@olesz: Hi,

Is there any update on this?
Is this going to be implemented in the near future?

Thanks!@olesz: @pekkaklarck How can I ask any of the maintainers to check the pull request I created on this enhancement? (https://github.com/robotframework/robotframework/pull/5244)@pekkaklarck: PR #5244 by @olesz looks so promising that I added this to RF 7.2 scope. The change is backwards incompatible, because earlier a SKIP in an iteration after a failed iteration causes the test to be skipped, but after the change the test will fail. I don't think this is so severe that the change needed to wait for a major release, though. Although this issue is now labeled as an enhancement, the current behavior where iteration failures are masked by skips could also be considered a bug.@pekkaklarck: Implemented in PR #5244 by @olesz. Big thanks for the contribution!@pekkaklarck: I tested how this works with templates using IF and FOR. To my small surprise everything mostly worked well, but there were problems at least with error reporting when using FOR. I'll reopen this, investigate problems and fix them.@olesz: Do you want me to do the debugging? If you share what you've found maybe I can help finding the root cause.@pekkaklarck: Thanks for the offer. I already found the root cause and I'm working with the fix. I just noticed another issue (#5276) and fix that first. @pekkaklarck: This example demonstrates the problem:
```robotframework
Example
    [Template]    Run Keyword
    FOR    ${x}    IN    a    b    c
        Skip    ${x}
    END
```@pekkaklarck: The following changes seem to fix the issue. I'm not sure is this the best fix yet, but I'll think about that more after lunch.

```diff
--- a/src/robot/running/bodyrunner.py
+++ b/src/robot/running/bodyrunner.py
@@ -56,18 +56,19 @@ class BodyRunner:
                 self._run = exception.can_continue(self._context, self._templated)
         if passed:
             raise passed
-        if errors and self._templated:
+        if errors and self._templated and any(e.skip for e in errors):
             errors = self._handle_skip_with_templates(errors, result)
         if errors:
             raise ExecutionFailures(errors)
 
     def _handle_skip_with_templates(self, errors, result):
-        if len(result.body) == 1 or not any(e.skip for e in errors):
+        if len(result.body) == 1 and not result.body[0].type == 'FOR':
             return errors
         if all(item.skipped for item in result.body):
             raise ExecutionFailed('All iterations skipped.', skip=True)
         return [e for e in errors if not e.skip]
 
+
 class KeywordRunner:
```@LucianCrainic: Hello @pekkaklarck, i just noticed that on a template if i want to skip a specific input case it skips the whole test and does not continue with the other input cases. 

Example of what i am talking about below,

```robotframework
*** Test Cases ***
Eat yellow fruits
    [Documentation]    Eat only yellow fruits, Fails if item given is a fruit which is not yellow and skip if item is not a fruit
    [Template]    Check fruit and eat it
    plane             # SKIP
    banana          # should be PASS but is not executed
    lemon            # should be PASS but is not executed 
```@pekkaklarck: @LucianCrainic This issue ought to change exactly that. The core functionality has already been implemented, but there's a small issue when using templates with FOR loops and that's why the issue was reopened. The change isn't available in any release yet, but we plan to create RF 7.2 beta 1 soon. If you want to test this earlier, the installation instructions explain how to install the code directly from GitHub.@pekkaklarck: It seems that the issue with FOR loops is best handled with `ForRunner`. It's even simpler than what I proposed earlier. I created plenty of new tests that all now pass. I'll run all acceptance tests locally and commit changes if there are no regressions.

Solution patch:
diff --git a/doc/userguide/src/CreatingTestData/CreatingTestCases.rst b/doc/userguide/src/CreatingTestData/CreatingTestCases.rst
index 5a3aad81d..f4541ac7b 100644
--- a/doc/userguide/src/CreatingTestData/CreatingTestCases.rst
+++ b/doc/userguide/src/CreatingTestData/CreatingTestCases.rst
@@ -948,6 +948,18 @@ are executed even if one or more of them fails. It is possible to use this
 kind of `continue on failure`_ mode with normal tests too, but with
 the templated tests the mode is on automatically.
 
+Also in case of templated tests all the rounds are executed even if one
+or more of them are skipped.
+The aggregated result of a templated test with skipped iterations is:
+
+* PASS if there were at lease one PASS iteration with any number of SKIPs
+* SKIP if all iterations were SKIP
+* FAIL if any of the iterations were FAIL
+
+.. note:: This behavior is valid only from Robot Framework 7.2.
+          Earlier versions stopped executing remaining rounds in case of a
+          skipped iteration is found
+
 .. sourcecode:: robotframework
 
    *** Settings ***
diff --git a/doc/userguide/src/CreatingTestData/Variables.rst b/doc/userguide/src/CreatingTestData/Variables.rst
index f55243458..e1299a827 100644
--- a/doc/userguide/src/CreatingTestData/Variables.rst
+++ b/doc/userguide/src/CreatingTestData/Variables.rst
@@ -115,7 +115,7 @@ its value as-is and the value can be any object. If the variable is not used
 alone, like `${GREER}, ${NAME}!!` above, its value is first converted into
 a string and then concatenated with the other data.
 
-.. note:: Variable values are used as-is without conversions also when
+.. note:: Variable values are used as-is without string conversion also when
           passing arguments to keywords using the `named arguments`_
           syntax like `argname=${var}`.
 
@@ -132,8 +132,6 @@ object:
      def __str__():
          return "Hi, terra!"
 
-
-
 With these two variables set, we then have the following test data:
 
 .. sourcecode:: robotframework
@@ -153,13 +151,58 @@ the arguments as explained below:
 - :name:`KW 3` gets a string `I said "Hello, world!"`
 - :name:`KW 4` gets a string `You said "Hi, terra!"`
 
-.. Note:: Converting variables to Unicode obviously fails if the variable
-          cannot be represented as Unicode. This can happen, for example,
-          if you try to use byte sequences as arguments to keywords so that
-          you catenate the values together like `${byte1}${byte2}`.
-          A workaround is creating a variable that contains the whole value
-          and using it alone in the cell (e.g. `${bytes}`) because then
-          the value is used as-is.
+Scalar variables containing bytes
+'''''''''''''''''''''''''''''''''
+
+Variables containing bytes__ or bytearrays__ are handled slightly differently
+than other variables containing non-string values:
+
+- If they are used alone, everything works exactly as with other objects and
+  their values are passed to keywords as-is.
+
+- If they are concatenated only with other variables that also contain bytes or
+  bytearrays, the result is bytes instead of a string.
+
+- If they are concatenated with strings or with variables containing other
+  types than bytes or bytearrays, they are converted to strings like other
+  objects, but they have a different string representation than they normally
+  have in Python. With Python the string representation contains surrounding
+  quotes and a `b` prefix like `b'\x00'`, but with Robot Framework quotes
+  and the prefix are omitted, and each byte is mapped to a Unicode code point
+  with the same ordinal. In practice this is same as converting bytes to strings
+  using the Latin-1 encoding. This format has a big benefit that the resulting
+  string can be converted back to bytes, for example, by using the BuiltIn_
+  keyword :name:`Convert To Bytes` or by automatic `argument conversion`_.
+
+The following examples demonstrates using bytes and bytearrays would work
+exactly the same way. Variable `${a}` is expected to contain bytes `\x00\x01`
+and variable `${b}` bytes `a\xe4`.
+
+.. sourcecode:: robotframework
+
+    *** Test Cases ***
+    Bytes alone
+        [Documentation]    Keyword gets bytes '\x00\x01'.
+        Keyword    ${a}
+
+    Bytes concatenated with bytes
+        [Documentation]    Keyword gets bytes '\x00\x01a\xe4'.
+        Keyword    ${a}${b}
+
+    Bytes concatenated with others
+        [Documentation]    Keyword gets string '=\x00\x01a\xe4='.
+        Keyword    =${a}${b}=
+
+__ https://docs.python.org/3/library/stdtypes.html#bytes-objects
+__ https://docs.python.org/3/library/stdtypes.html#bytearray-objects
+
+.. note:: Getting bytes when variables containing bytes are concatenated is new
+          in Robot Framework 7.2. With earlier versions the result was a string.
+
+.. note:: All bytes being mapped to matching Unicode code points in string
+          representation is new Robot Framework 7.2. With earlier versions
+          only bytes in the ASCII range were mapped directly code points and
+          other bytes were represented in an escaped format.
 
 .. _list variable:
 .. _list variables:
diff --git a/src/robot/errors.py b/src/robot/errors.py
index 4e1d3786a..2aaee2292 100644
--- a/src/robot/errors.py
+++ b/src/robot/errors.py
@@ -143,10 +143,12 @@ class ExecutionStatus(RobotError):
     def can_continue(self, context, templated=False):
         if context.dry_run:
             return True
-        if self.syntax or self.exit or self.skip or self.test_timeout:
+        if self.syntax or self.exit or self.test_timeout:
             return False
         if templated:
             return context.continue_on_failure(default=True)
+        if self.skip:
+            return False
         if self.keyword_timeout:
             return False
         return self.continue_on_failure or context.continue_on_failure()
diff --git a/src/robot/libraries/BuiltIn.py b/src/robot/libraries/BuiltIn.py
index 5d7417e4d..af5d6f845 100644
--- a/src/robot/libraries/BuiltIn.py
+++ b/src/robot/libraries/BuiltIn.py
@@ -341,8 +341,8 @@ class _Converter(_BuiltInBase):
         - ``text:`` Converts text to bytes character by character. All
           characters with ordinal below 256 can be used and are converted to
           bytes with same values. Many characters are easiest to represent
-          using escapes like ``\x00`` or ``\xff``. Supports both Unicode
-          strings and bytes.
+          using escapes like ``\x00`` or ``\xff``. In practice this is the same
+          as Latin-1 encoding.
 
         - ``int:`` Converts integers separated by spaces to bytes. Similarly as
           with `Convert To Integer`, it is possible to use binary, octal, or
@@ -3584,11 +3584,11 @@ class _Misc(_BuiltInBase):
 
         If the optional ``append`` argument is given a true value (see `Boolean
         arguments`), the given ``message`` is added after the possible earlier
-        message by joining the messages with a space.
-        
+        message.
+
         An optional ``separator`` argument can be used to provide custom separator
-        string when appending to old text. By default a single space is used 
-        as separator.
+        string when appending to the old text. A single space is used as separator
+        by default.
 
         In test teardown this keyword can alter the possible failure message,
         but otherwise failures override messages set by this keyword. Notice
@@ -3605,7 +3605,8 @@ class _Misc(_BuiltInBase):
         | Set Test Message | `*`HTML`*` <b>Hello!</b> |                      |
 
         This keyword can not be used in suite setup or suite teardown.
-        ``separator`` argument is new in Robot Framework 7.2.
+
+        The ``separator`` argument is new in Robot Framework 7.2.
         """
         test = self._context.test
         if not test:
@@ -3642,19 +3643,19 @@ class _Misc(_BuiltInBase):
     def set_test_documentation(self, doc, append=False, separator=' '):
         """Sets documentation for the current test case.
 
-        By default the possible existing documentation is overwritten, but
+        The possible existing documentation is overwritten by default, but
         this can be changed using the optional ``append`` argument similarly
         as with `Set Test Message` keyword.
-        
+
         An optional ``separator`` argument can be used to provide custom separator
-        string when appending to old text. By default a single space is used 
-        as separator.
+        string when appending to the old text. A single space is used as separator
+        by default.
 
         The current test documentation is available as a built-in variable
         ``${TEST DOCUMENTATION}``. This keyword can not be used in suite
         setup or suite teardown.
-        
-        ``separator`` argument is new in Robot Framework 7.2.
+
+        The ``separator`` argument is new in Robot Framework 7.2.
         """
         test = self._context.test
         if not test:
@@ -3675,15 +3676,15 @@ class _Misc(_BuiltInBase):
         If the optional ``top`` argument is given a true value (see `Boolean
         arguments`), the documentation of the top level suite is altered
         instead.
-        
+
         An optional ``separator`` argument can be used to provide custom separator
-        string when appending to old text. By default a single space is used 
-        as separator.
+        string when appending to the old text. A single space is used as separator
+        by default.
 
         The documentation of the current suite is available as a built-in
         variable ``${SUITE DOCUMENTATION}``.
-        
-        ``separator`` argument is new in Robot Framework 7.2.
+
+        The ``separator`` argument is new in Robot Framework 7.2.
         """
         suite = self._get_context(top).suite
         suite.doc = self._get_new_text(suite.doc, doc, append, separator=separator)
@@ -3700,22 +3701,22 @@ class _Misc(_BuiltInBase):
         This keyword sets the metadata of the current suite by default.
         If the optional ``top`` argument is given a true value (see `Boolean
         arguments`), the metadata of the top level suite is altered instead.
-        
+
         An optional ``separator`` argument can be used to provide custom separator
-        string when appending to old text. By default a single space is used 
-        as separator.
+        string when appending to the old text. A single space is used as separator
+        by default.
 
         The metadata of the current suite is available as a built-in variable
         ``${SUITE METADATA}`` in a Python dictionary. Notice that modifying this
         variable directly has no effect on the actual metadata the suite has.
-        
-        ``separator`` argument is new in Robot Framework 7.2.
+
+        The ``separator`` argument is new in Robot Framework 7.2.
         """
         if not is_string(name):
             name = str(name)
         metadata = self._get_context(top).suite.metadata
         original = metadata.get(name, '')
-        metadata[name] = self._get_new_text(original, value, append, 
+        metadata[name] = self._get_new_text(original, value, append,
                                             separator=separator)
         self._variables.set_suite('${SUITE_METADATA}', metadata.copy(), top)
         self.log(f"Set suite metadata '{name}' to value '{metadata[name]}'.")
diff --git a/src/robot/running/bodyrunner.py b/src/robot/running/bodyrunner.py
index bd17f2cba..f93a3a9fd 100644
--- a/src/robot/running/bodyrunner.py
+++ b/src/robot/running/bodyrunner.py
@@ -56,9 +56,17 @@ class BodyRunner:
                 self._run = exception.can_continue(self._context, self._templated)
         if passed:
             raise passed
+        if errors and self._templated:
+            errors = self._handle_skip_with_templates(errors, result)
         if errors:
             raise ExecutionFailures(errors)
 
+    def _handle_skip_with_templates(self, errors, result):
+        if len(result.body) == 1 or not any(e.skip for e in errors):
+            return errors
+        if all(item.skipped for item in result.body):
+            raise ExecutionFailed('All iterations skipped.', skip=True)
+        return [e for e in errors if not e.skip]
 
 class KeywordRunner:
 
diff --git a/src/robot/utils/unic.py b/src/robot/utils/unic.py
index fc8798291..9add91ef0 100644
--- a/src/robot/utils/unic.py
+++ b/src/robot/utils/unic.py
@@ -26,13 +26,11 @@ def _safe_str(item):
     if isinstance(item, str):
         return item
     if isinstance(item, (bytes, bytearray)):
-        try:
-            return item.decode('ASCII')
-        except UnicodeError:
-            return ''.join(chr(b) if b < 128 else '\\x%x' % b for b in item)
+        # Map each byte to Unicode code point with same ordinal.
+        return item.decode('latin-1')
     try:
         return str(item)
-    except:
+    except Exception:
         return _unrepresentable_object(item)
 
 
@@ -45,7 +43,7 @@ class PrettyRepr(PrettyPrinter):
     def format(self, object, context, maxlevels, level):
         try:
             return PrettyPrinter.format(self, object, context, maxlevels, level)
-        except:
+        except Exception:
             return _unrepresentable_object(object), True, False
 
     # Don't split strings: https://stackoverflow.com/questions/31485402
@@ -63,5 +61,6 @@ class PrettyRepr(PrettyPrinter):
 
 def _unrepresentable_object(item):
     from .error import get_error_message
-    return "<Unrepresentable object %s. Error: %s>" \
-           % (item.__class__.__name__, get_error_message())
+
+    error = get_error_message()
+    return f'<Unrepresentable object {type(item).__name__}. Error: {error}>'
diff --git a/src/robot/variables/replacer.py b/src/robot/variables/replacer.py
index aa8d8c8b6..a56a16c22 100644
--- a/src/robot/variables/replacer.py
+++ b/src/robot/variables/replacer.py
@@ -59,14 +59,11 @@ class VariableReplacer:
         result = []
         for item in items:
             match = search_variable(item, ignore_errors=ignore_errors)
-            if not match:
-                result.append(unescape(item))
+            value = self._replace(match, ignore_errors)
+            if match.is_list_variable() and is_list_like(value):
+                result.extend(value)
             else:
-                value = self._replace_scalar(match, ignore_errors)
-                if match.is_list_variable() and is_list_like(value):
-                    result.extend(value)
-                else:
-                    result.append(value)
+                result.append(value)
         return result
 
     def replace_scalar(self, item, ignore_errors=False):
@@ -80,37 +77,36 @@ class VariableReplacer:
             match = item
         else:
             match = search_variable(item, ignore_errors=ignore_errors)
-        if not match:
-            return unescape(match.string)
-        return self._replace_scalar(match, ignore_errors)
-
-    def _replace_scalar(self, match, ignore_errors=False):
-        if match.is_variable():
-            return self._get_variable_value(match, ignore_errors)
-        return self._replace_string(match, unescape, ignore_errors)
+        return self._replace(match, ignore_errors)
 
     def replace_string(self, item, custom_unescaper=None, ignore_errors=False):
         """Replaces variables from a string. Result is always a string.
 
         Input can also be an already found VariableMatch.
         """
-        unescaper = custom_unescaper or unescape
         if isinstance(item, VariableMatch):
             match = item
         else:
             match = search_variable(item, ignore_errors=ignore_errors)
-        if not match:
-            return safe_str(unescaper(match.string))
-        return self._replace_string(match, unescaper, ignore_errors)
+        result = self._replace(match, ignore_errors, custom_unescaper or unescape)
+        return safe_str(result)
 
-    def _replace_string(self, match, unescaper, ignore_errors):
+    def _replace(self, match, ignore_errors, unescaper=unescape):
+        if not match:
+            return unescaper(match.string)
+        if match.is_variable():
+            return self._get_variable_value(match, ignore_errors)
         parts = []
         while match:
-            parts.append(unescaper(match.before))
-            parts.append(safe_str(self._get_variable_value(match, ignore_errors)))
+            if match.before:
+                parts.append(unescaper(match.before))
+            parts.append(self._get_variable_value(match, ignore_errors))
             match = search_variable(match.after, ignore_errors=ignore_errors)
-        parts.append(unescaper(match.string))
-        return ''.join(parts)
+        if match.string:
+            parts.append(unescaper(match.string))
+        if all(isinstance(p, (bytes, bytearray)) for p in parts):
+            return b''.join(parts)
+        return ''.join(safe_str(p) for p in parts)
 
     def _get_variable_value(self, match, ignore_errors):
         match.resolve_base(self, ignore_errors)


Test patch:
diff --git a/atest/testdata/running/expbytevalues.py b/atest/testdata/running/expbytevalues.py
index 3547c2b3a..dd2598c91 100644
--- a/atest/testdata/running/expbytevalues.py
+++ b/atest/testdata/running/expbytevalues.py
@@ -1,5 +1,5 @@
 VARIABLES = dict(exp_return_value=b'ty\xf6paikka',
-                 exp_return_msg='ty\\xf6paikka',
+                 exp_return_msg='työpaikka',
                  exp_error_msg="b'hyv\\xe4'",
                  exp_log_msg="b'\\xe4iti'",
                  exp_log_multiline_msg="b'\\xe4iti\\nis\\xe4'")
diff --git a/atest/testdata/variables/non_string_variables.py b/atest/testdata/variables/non_string_variables.py
index 4e513c602..a5c2669da 100644
--- a/atest/testdata/variables/non_string_variables.py
+++ b/atest/testdata/variables/non_string_variables.py
@@ -4,15 +4,15 @@ import sys
 def get_variables():
     variables = {'integer': 42,
                  'float': 3.14,
-                 'byte_string': b'hyv\xe4',
-                 'byte_string_str': 'hyv\\xe4',
+                 'bytes': b'hyv\xe4',
+                 'bytearray': bytearray(b'hyv\xe4'),
+                 'low_bytes': b'\x00\x01\x02',
                  'boolean': True,
                  'none': None,
                  'module': sys,
                  'module_str': str(sys),
                  'list': [1, b'\xe4', '\xe4'],
                  'dict': {b'\xe4': '\xe4'},
-                 'list_str': u"[1, b'\\xe4', '\xe4']",
-                 'dict_str': u"{b'\\xe4': '\xe4'}"}
+                 'list_str': "[1, b'\\xe4', '\xe4']",
+                 'dict_str': "{b'\\xe4': '\xe4'}"}
     return variables
-
diff --git a/utest/running/test_argumentspec.py b/utest/running/test_argumentspec.py
index 465a3b1bc..79cef6b90 100644
--- a/utest/running/test_argumentspec.py
+++ b/utest/running/test_argumentspec.py
@@ -23,7 +23,7 @@ class TestStringRepr(unittest.TestCase):
 
     def test_default_as_bytes(self):
         self._verify('b=ytes', ['b'], defaults={'b': b'ytes'})
-        self._verify('ä=\\xe4', ['ä'], defaults={'ä': b'\xe4'})
+        self._verify('ä=\xe4', ['ä'], defaults={'ä': b'\xe4'})
 
     def test_type_as_class(self):
         self._verify('a: int, b: bool', ['a', 'b'], types={'a': int, 'b': bool})
diff --git a/utest/utils/test_misc.py b/utest/utils/test_misc.py
index 6b92c180f..bcfa74620 100644
--- a/utest/utils/test_misc.py
+++ b/utest/utils/test_misc.py
@@ -28,7 +28,7 @@ class TestSeg2Str(unittest.TestCase):
         self._verify([b'ascii'], "'ascii'")
 
     def test_non_ascii_bytes(self):
-        self._verify([b'non-\xe4scii'], "'non-\\xe4scii'")
+        self._verify([b'non-\xe4scii'], "'non-\xe4scii'")
 
     def test_other_objects(self):
         self._verify([None, 1, True], "'None', '1' and 'True'")
diff --git a/utest/utils/test_old_py23_compatibility_layer.py b/utest/utils/test_old_py23_compatibility_layer.py
index c4fbda5f5..1ab19eaa2 100644
--- a/utest/utils/test_old_py23_compatibility_layer.py
+++ b/utest/utils/test_old_py23_compatibility_layer.py
@@ -82,7 +82,7 @@ class TestCompatibilityLayer(unittest.TestCase):
         with self.validate_deprecation('unic'):
             assert_equal(utils.unic(42), '42')
         with self.validate_deprecation('unic'):
-            assert_equal(utils.unic(b'Hyv\xe4'), r'Hyv\xe4')
+            assert_equal(utils.unic(b'Hyv\xe4'), 'Hyvä')
         with self.validate_deprecation('unic'):
             assert_equal(utils.unic(b'Paha'), 'Paha')
 
diff --git a/utest/utils/test_unic.py b/utest/utils/test_unic.py
index a6fb06cea..a96dfc300 100644
--- a/utest/utils/test_unic.py
+++ b/utest/utils/test_unic.py
@@ -23,20 +23,18 @@ class TestSafeStr(unittest.TestCase):
         result = safe_str(objects)
         assert_equal(result, '[Hyvä, Hyvä]')
 
-    def test_bytes_below_128(self):
+    def test_bytes(self):
         assert_equal(safe_str('\x00-\x01-\x02-\x7f'), '\x00-\x01-\x02-\x7f')
-
-    def test_bytes_above_128(self):
-        assert_equal(safe_str(b'hyv\xe4'), 'hyv\\xe4')
-        assert_equal(safe_str(b'\x00-\x01-\x02-\xe4'), '\x00-\x01-\x02-\\xe4')
+        assert_equal(safe_str(b'hyv\xe4'), 'hyvä')
+        assert_equal(safe_str(b'\x00-\x01-\x02-\xe4-\xff'), '\x00-\x01-\x02-\xe4-\xff')
 
     def test_bytes_with_newlines_tabs_etc(self):
-        assert_equal(safe_str(b"\x00\xe4\n\t\r\\'"), "\x00\\xe4\n\t\r\\'")
+        assert_equal(safe_str(b"\x00\xe4\n\t\r\\'"), "\x00\xe4\n\t\r\\'")
 
     def test_bytearray(self):
-        assert_equal(safe_str(bytearray(b'hyv\xe4')), 'hyv\\xe4')
-        assert_equal(safe_str(bytearray(b'\x00-\x01-\x02-\xe4')), '\x00-\x01-\x02-\\xe4')
-        assert_equal(safe_str(bytearray(b"\x00\xe4\n\t\r\\'")), "\x00\\xe4\n\t\r\\'")
+        assert_equal(safe_str(bytearray(b'hyv\xe4')), 'hyv\xe4')
+        assert_equal(safe_str(bytearray(b'\x00-\x01-\x02-\xe4')), '\x00-\x01-\x02-\xe4')
+        assert_equal(safe_str(bytearray(b"\x00\xe4\n\t\r\\'")), "\x00\xe4\n\t\r\\'")
 
     def test_failure_in_str(self):
         failing = StrFails()