text: #include "utils.h"
  
  #include <algorithm>
  #include <chrono>
  #include <ctype.h>
  #include <fstream>
  #include <sys/stat.h>
  #include <sys/types.h>
  #include <tuple>
  #include <unistd.h>
  
  #include "3rd-party/catch.hpp"
  #include "htmlrenderer.h"
  #include "test-helpers/chdir.h"
  #include "test-helpers/envvar.h"
  #include "test-helpers/stringmaker/optional.h"
  #include "test-helpers/tempdir.h"
  #include "test-helpers/tempfile.h"
  
  using namespace newsboat;
  
  TEST_CASE("tokenize() extracts tokens separated by given delimiters", "[utils]")
  {
  	std::vector<std::string> tokens;
  
  	SECTION("Default delimiters") {
  		tokens = utils::tokenize("as df qqq");
  		REQUIRE(tokens.size() == 3);
  		REQUIRE(tokens[0] == "as");
  		REQUIRE(tokens[1] == "df");
  		REQUIRE(tokens[2] == "qqq");
  
  		tokens = utils::tokenize(" aa ");
  		REQUIRE(tokens.size() == 1);
  		REQUIRE(tokens[0] == "aa");
  
  		tokens = utils::tokenize("	");
  		REQUIRE(tokens.size() == 0);
  
  		tokens = utils::tokenize("");
  		REQUIRE(tokens.size() == 0);
  	}
  
  	SECTION("Splitting by tabulation characters") {
  		tokens = utils::tokenize("hello world\thow are you?", "\t");
  		REQUIRE(tokens.size() == 2);
  		REQUIRE(tokens[0] == "hello world");
  		REQUIRE(tokens[1] == "how are you?");
  	}
  }
  
  TEST_CASE(
  	"tokenize_spaced() splits string into runs of delimiter characters "
  	"interspersed with runs of non-delimiter chars",
  	"[utils]")
  {
  	std::vector<std::string> tokens;
  
  	SECTION("Default delimiters include space and tab") {
  		tokens = utils::tokenize_spaced("a b");
  		REQUIRE(tokens.size() == 3);
  		REQUIRE(tokens[1] == " ");
  
  		tokens = utils::tokenize_spaced(" a\t b ");
  		REQUIRE(tokens.size() == 5);
  		REQUIRE(tokens[0] == " ");
  		REQUIRE(tokens[1] == "a");
  		REQUIRE(tokens[2] == "\t ");
  		REQUIRE(tokens[3] == "b");
  		REQUIRE(tokens[4] == " ");
  	}
  
  	SECTION("Comma-separated values containing spaces and tabs") {
  		tokens = utils::tokenize_spaced("123,John Doe,\t\t$8", ",");
  		REQUIRE(tokens.size() == 5);
  		REQUIRE(tokens[0] == "123");
  		REQUIRE(tokens[1] == ",");
  		REQUIRE(tokens[2] == "John Doe");
  		REQUIRE(tokens[3] == ",");
  		REQUIRE(tokens[4] == "\t\t$8");
  	}
  }
  
  TEST_CASE(
  	"tokenize_quoted() splits string on delimiters, treating strings "
  	"inside double quotes as single token",
  	"[utils]")
  {
  	std::vector<std::string> tokens;
  
  	SECTION("Default delimiters include spaces, newlines and tabs") {
  		tokens = utils::tokenize_quoted(
  				"asdf \"foobar bla\" \"foo\\r\\n\\tbar\"");
  		REQUIRE(tokens.size() == 3);
  		REQUIRE(tokens[0] == "asdf");
  		REQUIRE(tokens[1] == "foobar bla");
  		REQUIRE(tokens[2] == "foo\r\n\tbar");
  
  		tokens = utils::tokenize_quoted("  \"foo \\\\xxx\"\t\r \" \"");
  		REQUIRE(tokens.size() == 2);
  		REQUIRE(tokens[0] == "foo \\xxx");
  		REQUIRE(tokens[1] == " ");
  	}
  
  	SECTION("Closing double quote marks the end of a token") {
  		tokens = utils::tokenize_quoted(R"(set browser "mpv %u";)");
  
  		REQUIRE(tokens.size() == 4);
  		REQUIRE(tokens[0] == "set");
  		REQUIRE(tokens[1] == "browser");
  		REQUIRE(tokens[2] == "mpv %u");
  		REQUIRE(tokens[3] == ";");
  	}
  }
  
  TEST_CASE("tokenize_quoted() implicitly closes quotes at the end of the string",
  	"[utils]")
  {
  	std::vector<std::string> tokens;
  
  	tokens = utils::tokenize_quoted("\"\\\\");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == "\\");
  
  	tokens = utils::tokenize_quoted("\"\\\\\" and \"some other stuff");
  	REQUIRE(tokens.size() == 3);
  	REQUIRE(tokens[0] == "\\");
  	REQUIRE(tokens[1] == "and");
  	REQUIRE(tokens[2] == "some other stuff");
  
  	tokens = utils::tokenize_quoted(R"("abc\)");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == "abc");
  }
  
  TEST_CASE(
  	"tokenize_quoted() interprets \"\\\\\" as escaped backslash and puts "
  	"single backslash in output",
  	"[utils]")
  {
  	std::vector<std::string> tokens;
  
  	tokens = utils::tokenize_quoted(R"_("")_");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == "");
  
  	tokens = utils::tokenize_quoted(R"_("\\")_");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == R"_(\)_");
  
  	tokens = utils::tokenize_quoted(R"_("#\\")_");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == R"_(#\)_");
  
  	tokens = utils::tokenize_quoted(R"_("'#\\'")_");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == R"_('#\')_");
  
  	tokens = utils::tokenize_quoted(R"_("'#\\ \\'")_");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == R"_('#\ \')_");
  
  	tokens = utils::tokenize_quoted("\"\\\\\\\\");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == "\\\\");
  
  	tokens = utils::tokenize_quoted("\"\\\\\\\\\\\\");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == "\\\\\\");
  
  	tokens = utils::tokenize_quoted("\"\\\\\\\\\"");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == "\\\\");
  
  	tokens = utils::tokenize_quoted("\"\\\\\\\\\\\\\"");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == "\\\\\\");
  
  	// https://github.com/newsboat/newsboat/issues/642
  	tokens = utils::tokenize_quoted(R"("\\bgit\\b")");
  	REQUIRE(tokens.size() == 1);
  	REQUIRE(tokens[0] == R"(\bgit\b)");
  
  	// https://github.com/newsboat/newsboat/issues/536
  	tokens = utils::tokenize_quoted(
  			R"(browser "/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --app %u")");
  	REQUIRE(tokens.size() == 2);
  	REQUIRE(tokens[0] == "browser");
  	REQUIRE(tokens[1] ==
  		R"(/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --app %u)");
  }
  
  TEST_CASE("tokenize_quoted() doesn't un-escape escaped backticks", "[utils]")
  {
  	std::vector<std::string> tokens;
  
  	tokens = utils::tokenize_quoted("asdf \"\\`foobar `bla`\\`\"");
  
  	REQUIRE(tokens.size() == 2);
  	REQUIRE(tokens[0] == "asdf");
  	REQUIRE(tokens[1] == "\\`foobar `bla`\\`");
  }
  
  TEST_CASE("tokenize_quoted stops tokenizing once it found a # character "
  	"(outside of double quotes)",
  	"[utils]")
  {
  	std::vector<std::string> tokens;
  
  	SECTION("A string consisting of just a comment") {
  		tokens = utils::tokenize_quoted("# just a comment");
  		REQUIRE(tokens.empty());
  	}
  
  	SECTION("A string with one quoted substring") {
  		tokens = utils::tokenize_quoted(R"#("a test substring" # !!!)#");
  		REQUIRE(tokens.size() == 1);
  		REQUIRE(tokens[0] == "a test substring");
  	}
  
  	SECTION("A string with two quoted substrings") {
  		tokens = utils::tokenize_quoted(R"#("first sub" "snd" # comment)#");
  		REQUIRE(tokens.size() == 2);
  		REQUIRE(tokens[0] == "first sub");
  		REQUIRE(tokens[1] == "snd");
  	}
  
  	SECTION("A comment containing # character") {
  		tokens = utils::tokenize_quoted(R"#(one # a comment with # char)#");
  		REQUIRE(tokens.size() == 1);
  		REQUIRE(tokens[0] == "one");
  	}
  
  	SECTION("A # character inside quoted substring is ignored") {
  		tokens = utils::tokenize_quoted(R"#(this "will # be" ignored)#");
  		REQUIRE(tokens.size() == 3);
  		REQUIRE(tokens[0] == "this");
  		REQUIRE(tokens[1] == "will # be");
  		REQUIRE(tokens[2] == "ignored");
  	}
  }
  
  TEST_CASE("tokenize_quoted does not consider escaped pound sign (\\#) "
  	"a beginning of a comment",
  	"[utils]")
  {
  	const auto tokens = utils::tokenize_quoted(R"#(one \# two three # ???)#");
  	REQUIRE(tokens.size() == 4);
  	REQUIRE(tokens[0] == "one");
  	REQUIRE(tokens[1] == "\\#");
  	REQUIRE(tokens[2] == "two");
  	REQUIRE(tokens[3] == "three");
  }
  
  TEST_CASE("extract_token_quoted() returns no result if it finds a comment (signaled by '#')",
  	"[utils]")
  {
  	SECTION("actual comment") {
  		std::string str = "\t\t  # commented out";
  		auto token = utils::extract_token_quoted(str);
  		REQUIRE_FALSE(token.has_value());
  		REQUIRE(str == "");
  	}
  
  	SECTION("ignores '#' if it is inside quotes") {
  		std::string str = R"("# in quoted token" other tokens)";
  		auto token = utils::extract_token_quoted(str);
  		REQUIRE(token.has_value());
  		REQUIRE(token.value() == "# in quoted token");
  		REQUIRE(str == " other tokens");
  	}
  
  	SECTION("token before start of comment") {
  		std::string str = "some-token # some comment";
  		auto token = utils::extract_token_quoted(str);
  		REQUIRE(token.has_value());
  		REQUIRE(token.value() == "some-token");
  		REQUIRE(str == " # some comment");
  	}
  }
  
  TEST_CASE("extract_token_quoted() ignores configured `delimiters` in front of tokens",
  	"[utils]")
  {
  	SECTION("default delimiters") {
  		std::string str = "\n\r\t \n\t\r token-name";
  		auto token = utils::extract_token_quoted(str);
  		REQUIRE(token.has_value());
  		REQUIRE(token.value() == "token-name");
  		REQUIRE(str == "");
  	}
  
  	SECTION("empty delimiters") {
  		std::string str = "\n\r\t \n\t\r token-name";
  		auto token = utils::extract_token_quoted(str, "");
  		REQUIRE(token.has_value());
  		REQUIRE(token.value() == "\n\r\t \n\t\r token-name");
  		REQUIRE(str == "");
  	}
  
  	SECTION("single delimiter") {
  		std::string str = "--token name--";
  		auto token = utils::extract_token_quoted(str, "-");
  		REQUIRE(token.has_value());
  		REQUIRE(token.value() == "token name");
  		REQUIRE(str == "--");
  	}
  
  	SECTION("two delimiters") {
  		std::string str = "--token name--";
  		auto token = utils::extract_token_quoted(str, " -");
  		REQUIRE(token.has_value());
  		REQUIRE(token.value() == "token");
  		REQUIRE(str == " name--");
  	}
  }
  
  TEST_CASE("extract_token_quoted() ignores delimiter characters within quoted strings",
  	"[utils]")
  {
  	SECTION("default delimiters") {
  		std::string str = R"(  "token name"  )";
  		auto token = utils::extract_token_quoted(str);
  		REQUIRE(token.has_value());
  		REQUIRE(token.value() == "token name");
  		REQUIRE(str == "  ");
  	}
  
  	SECTION("two delimiters") {
  		std::string str = R"(--"token name"--)";
  		auto token = utils::extract_token_quoted(str, " -");
  		REQUIRE(token.has_value());
  		REQUIRE(token.value() == "token name");
  		REQUIRE(str == "--");
  	}
  }
  
  TEST_CASE("extract_token_quoted() processes escape sequences within quoted strings",
  	"[utils]")
  {
  	std::string str = R"(  "\n \r \t \" \` \\ " remainder)";
  	auto token = utils::extract_token_quoted(str);
  	REQUIRE(token.has_value());
  	REQUIRE(token.value() == "\n \r \t \" \\` \\ ");
  	REQUIRE(str == " remainder");
  }
  
  TEST_CASE("extract_token_quoted() works with Unicode strings too", "[utils]")
  {
  	std::string str = R"("привет мир" Юникода)";
  	const auto token = utils::extract_token_quoted(str);
  	REQUIRE(token.has_value());
  	REQUIRE(token.value() == "привет мир");
  	REQUIRE(str == " Юникода");
  }
  
  TEST_CASE("tokenize_nl() split a string into delimiters and fields", "[utils]")
  {
  	std::vector<std::string> tokens;
  
  	SECTION("a few words separated by newlines") {
  		tokens = utils::tokenize_nl("first\nsecond\nthird");
  
  		REQUIRE(tokens.size() == 5);
  		REQUIRE(tokens[0] == "first");
  		REQUIRE(tokens[2] == "second");
  		REQUIRE(tokens[4] == "third");
  	}
  
  	SECTION("several preceding delimiters") {
  		tokens = utils::tokenize_nl("\n\n\nonly");
  
  		REQUIRE(tokens.size() == 4);
  		REQUIRE(tokens[3] == "only");
  	}
  
  	SECTION("redundant internal delimiters") {
  		tokens = utils::tokenize_nl("first\nsecond\n\nthird");
  
  		REQUIRE(tokens.size() == 6);
  		REQUIRE(tokens[0] == "first");
  		REQUIRE(tokens[2] == "second");
  		REQUIRE(tokens[5] == "third");
  	}
  
  	SECTION("custom delimiter") {
  		tokens = utils::tokenize_nl("first\nsecond\nthird","i");
  
  		REQUIRE(tokens.size() == 5);
  		REQUIRE(tokens[0] == "f");
  		REQUIRE(tokens[2] == "rst\nsecond\nth");
  		REQUIRE(tokens[4] == "rd");
  	}
  
  	SECTION("no non-delimiter text") {
  		SECTION("single newline") {
  			tokens = utils::tokenize_nl("\n");
  
  			REQUIRE(tokens.size() == 1);
  			REQUIRE(tokens[0] == "\n");
  		}
  
  		SECTION("multiple newlines") {
  			tokens = utils::tokenize_nl("\n\n\n");
  
  			REQUIRE(tokens.size() == 3);
  			REQUIRE(tokens[0] == "\n");
  			REQUIRE(tokens[1] == "\n");
  			REQUIRE(tokens[2] == "\n");
  		}
  	}
  }
  
  TEST_CASE(
  	"strip_comments returns only the part of the line before first # character",
  	"[utils]")
  {
  	SECTION("no comments in line") {
  		REQUIRE(utils::strip_comments("") == "");
  		REQUIRE(utils::strip_comments("\t\n") == "\t\n");
  		REQUIRE(utils::strip_comments("some directive ") == "some directive ");
  	}
  
  	SECTION("fully commented line") {
  		REQUIRE(utils::strip_comments("#") == "");
  		REQUIRE(utils::strip_comments("# #") == "");
  		REQUIRE(utils::strip_comments("# comment") == "");
  	}
  
  	SECTION("partially commented line") {
  		REQUIRE(utils::strip_comments("directive # comment") == "directive ");
  		REQUIRE(utils::strip_comments("directive # comment # another") == "directive ");
  		REQUIRE(utils::strip_comments("directive#comment") == "directive");
  	}
  }
  
  TEST_CASE("strip_comments ignores escaped # characters (\\#)")
  {
  	const auto expected =
  		std::string(R"#(one two \# three four)#");
  	const auto input = expected + "# and a comment";
  	REQUIRE(utils::strip_comments(input) == expected);
  }
  
  TEST_CASE("strip_comments ignores # characters inside double quotes",
  	"[utils][issue652]")
  {
  	SECTION("Real-world cases from issue 652") {
  		const auto expected1 =
  			std::string(R"#(highlight article "[-=+#_*~]{3,}.*" green default)#");
  		const auto input1 = expected1 + "# this is a comment";
  		REQUIRE(utils::strip_comments(input1) == expected1);
  
  		const auto expected2 =
  			std::string(
  				R"#(highlight all "(https?|ftp)://[\-\.,/%~_:?&=\#a-zA-Z0-9]+" blue default bold)#");
  		const auto input2 = expected2 + "#heresacomment";
  		REQUIRE(utils::strip_comments(input2) == expected2);
  	}
  
  	SECTION("Escaped double quote inside double quotes is not treated "
  		"as closing quote") {
  		const auto expected =
  			std::string(R"#(test "here \"goes # nothing\" etc" hehe)#");
  		const auto input = expected + "# and here is a comment";
  		REQUIRE(utils::strip_comments(input) == expected);
  	}
  }
  
  TEST_CASE("strip_comments ignores # characters inside backticks", "[utils]")
  {
  	SECTION("Simple case") {
  		const auto expected = std::string(R"#(one `two # three` four)#");
  		const auto input = expected + "# and a comment, of course";
  		REQUIRE(utils::strip_comments(input) == expected);
  	}
  
  	SECTION("Escaped backtick inside backticks is not treated as closing") {
  		const auto expected =
  			std::string(R"#(some `other \` tricky # test` hehe)#");
  		const auto input = expected + "#here goescomment";
  		REQUIRE(utils::strip_comments(input) == expected);
  	}
  }
  
  TEST_CASE("strip_comments is not confused by nested double quotes and backticks",
  	"[utils]")
  {
  	{
  		const auto expected = std::string(R"#("`" ... ` `"` ")#");
  		const auto input = expected + "#comment";
  		REQUIRE(utils::strip_comments(input) == expected);
  	}
  
  	{
  		const auto expected = std::string(R"#(aaa ` bbb "ccc ddd" e` dd)#");
  		const auto input = expected + "# a comment string";
  		REQUIRE(utils::strip_comments(input) == expected);
  	}
  
  	{
  		const auto expected =
  			std::string(R"#(option "this `weird " command` for value")#");
  		const auto input = expected + "#and a comment";
  		REQUIRE(utils::strip_comments(input) == expected);
  	}
  }
  
  TEST_CASE(
  	"consolidate_whitespace replaces multiple consecutive"
  	"whitespace with a single space",
  	"[utils]")
  {
  	REQUIRE(utils::consolidate_whitespace("LoremIpsum") == "LoremIpsum");
  	REQUIRE(utils::consolidate_whitespace("Lorem Ipsum") == "Lorem Ipsum");
  	REQUIRE(utils::consolidate_whitespace(" Lorem \t\tIpsum \t ") ==
  		" Lorem Ipsum ");
  	REQUIRE(utils::consolidate_whitespace(" Lorem \r\n\r\n\tIpsum") ==
  		" Lorem Ipsum");
  
  	REQUIRE(utils::consolidate_whitespace("") == "");
  }
  
  TEST_CASE("consolidate_whitespace preserves leading whitespace", "[utils]")
  {
  	REQUIRE(utils::consolidate_whitespace("    Lorem \t\tIpsum \t ") ==
  		"    Lorem Ipsum ");
  	REQUIRE(utils::consolidate_whitespace("   Lorem \r\n\r\n\tIpsum") ==
  		"   Lorem Ipsum");
  }
  
  TEST_CASE("get_command_output()", "[utils]")
  {
  	REQUIRE(utils::get_command_output("ls /dev/null") == "/dev/null\n");
  	REQUIRE_NOTHROW(utils::get_command_output(
  			"a-program-that-is-guaranteed-to-not-exists"));
  	REQUIRE(utils::get_command_output(
  			"a-program-that-is-guaranteed-to-not-exists") == "");
  	REQUIRE(utils::get_command_output("echo c\" d e") == "");
  }
  
  TEST_CASE("extract_filter()", "[utils]")
  {
  	{
  		const auto parts =
  			utils::extract_filter("filter:~/bin/script.sh:https://newsboat.org");
  		REQUIRE(std::string(parts.script_name) == "~/bin/script.sh");
  		REQUIRE(std::string(parts.url) == "https://newsboat.org");
  	}
  
  	{
  		const auto parts = utils::extract_filter("filter::https://newsboat.org");
  		REQUIRE(std::string(parts.script_name) == "");
  		REQUIRE(std::string(parts.url) == "https://newsboat.org");
  	}
  
  	{
  		const auto parts = utils::extract_filter("filter:https://newsboat.org");
  		REQUIRE(std::string(parts.script_name) == "https");
  		REQUIRE(std::string(parts.url) == "//newsboat.org");
  	}
  
  	{
  		const auto parts = utils::extract_filter("filter:foo:");
  		REQUIRE(std::string(parts.script_name) == "foo");
  		REQUIRE(std::string(parts.url) == "");
  	}
  
  	{
  		const auto parts = utils::extract_filter("filter:");
  		REQUIRE(std::string(parts.script_name) == "");
  		REQUIRE(std::string(parts.url) == "");
  	}
  }
  
  TEST_CASE("run_program()", "[utils]")
  {
  	const char* argv[4];
  	argv[0] = "cat";
  	argv[1] = nullptr;
  	REQUIRE(utils::run_program(
  			argv, "this is a multine-line\ntest string") ==
  		"this is a multine-line\ntest string");
  
  	argv[0] = "echo";
  	argv[1] = "-n";
  	argv[2] = "hello world";
  	argv[3] = nullptr;
  	REQUIRE(utils::run_program(argv, "") == "hello world");
  }
  
  TEST_CASE("run_command() executes the given command with a given argument",
  	"[utils]")
  {
  	TestHelpers::TempFile sentry;
  	const auto argument = sentry.get_path();
  
  	{
  		INFO("File shouldn't exist, because TempFile doesn't create it");
  
  		struct stat sb;
  		const int result = ::stat(argument.c_str(), &sb);
  		const int saved_errno = errno;
  
  		REQUIRE(result == -1);
  		REQUIRE(saved_errno == ENOENT);
  	}
  
  	utils::run_command("touch", argument);
  
  	struct stat sb;
  	int result = 0;
  
  	// Busy-wait for 100 tries of 10 milliseconds each, waiting for `touch` to
  	// create the file. Usually it happens quickly, and the loop exists on the
  	// first try; but sometimes on CI it takes longer for `touch` to finish, so
  	// we need a slightly longer wait.
  	int tries = 100;
  	while (tries-- > 0) {
  		::usleep(10 * 1000);
  
  		result = ::stat(argument.c_str(), &sb);
  		if (result == 0) {
  			break;
  		}
  	}
  
  	INFO("File should have been created by the `touch`");
  	REQUIRE(result == 0);
  }
  
  TEST_CASE("run_command() doesn't wait for the command to finish",
  	"[utils]")
  {
  	using namespace std::chrono;
  
  	const auto start = steady_clock::now();
  
  	// Using a big timeout value of 60 seconds to overcome any slowdowns that
  	// Cirrus CI sometimes exhibits, e.g. here waiting for `sleep 5` took 19
  	// seconds: https://cirrus-ci.com/task/6641382309756928?command=test#L64
  	utils::run_command("sleep", "60");
  
  	const auto finish = steady_clock::now();
  	const auto runtime = duration_cast<milliseconds>(finish - start);
  
  	REQUIRE(runtime.count() < 60000);
  }
  
  TEST_CASE("resolve_tilde() replaces ~ with the path to the home directory",
  	"[utils]")
  {
  	TestHelpers::EnvVar envVar("HOME");
  	envVar.set("test");
  	REQUIRE(utils::resolve_tilde("~") == "test/");
  	REQUIRE(utils::resolve_tilde("~/") == "test/");
  	REQUIRE(utils::resolve_tilde("~/dir") == "test/dir");
  	REQUIRE(utils::resolve_tilde("/home/~") == "/home/~");
  	REQUIRE(
  		utils::resolve_tilde("~/foo/bar") ==
  		"test/foo/bar"
  	);
  	REQUIRE(utils::resolve_tilde("/foo/bar") == "/foo/bar");
  }
  
  TEST_CASE("resolve_relative() returns an absolute file path relative to another",
  	"[utils]")
  {
  	SECTION("Nothing - absolute path") {
  		REQUIRE(utils::resolve_relative("/foo/bar", "/baz") == "/baz");
  		REQUIRE(utils::resolve_relative("/config", "/config/baz") == "/config/baz");
  	}
  	SECTION("Reference path") {
  		REQUIRE(utils::resolve_relative("/foo/bar", "baz") == "/foo/baz");
  		REQUIRE(utils::resolve_relative("/config", "baz") == "/baz");
  	}
  }
  
  TEST_CASE("replace_all()", "[utils]")
  {
  	REQUIRE(utils::replace_all("aaa", "a", "b") == "bbb");
  	REQUIRE(utils::replace_all("aaa", "aa", "ba") == "baa");
  	REQUIRE(utils::replace_all("aaaaaa", "aa", "ba") == "bababa");
  	REQUIRE(utils::replace_all("", "a", "b") == "");
  	REQUIRE(utils::replace_all("aaaa", "b", "c") == "aaaa");
  	REQUIRE(utils::replace_all("this is a normal test text", " t", " T") ==
  		"this is a normal Test Text");
  	REQUIRE(utils::replace_all("o o o", "o", "<o>") == "<o> <o> <o>");
  }
  
  TEST_CASE("replace_all() with from/to pairs", "[utils]")
  {
  	SECTION("output == input if no from/to pairs are specified") {
  		const std::string input = "lorem ipsum";
  		REQUIRE(utils::replace_all("", {}) == "");
  		REQUIRE(utils::replace_all(input, {}) == input);
  	}
  
  	SECTION("supports multiple from/to pairs") {
  		const std::vector<std::pair<std::string, std::string>> rules = {
  			{"a", "x"},
  			{"bb", "y"},
  			{"c", "zz"}
  		};
  
  		SECTION("empty input results in empty output") {
  			REQUIRE(utils::replace_all("", rules) == "");
  		}
  
  		SECTION("a from/to pair can be matched multiple times") {
  			REQUIRE(utils::replace_all("aaa", rules) == "xxx");
  			REQUIRE(utils::replace_all("bbb", rules) == "yb");
  			REQUIRE(utils::replace_all("ccc", rules) == "zzzzzz");
  		}
  
  		SECTION("multiple from/to pairs can be matched in the same input") {
  			REQUIRE(utils::replace_all("abc cccbbbaaa", rules) == "xbzz zzzzzzybxxx");
  			REQUIRE(utils::replace_all("begin abc cccbbbaaa end",
  					rules) == "begin xbzz zzzzzzybxxx end");
  		}
  	}
  
  	SECTION("multiple from/to pairs do not create loop") {
  		const std::vector<std::pair<std::string, std::string>> rules = {
  			{"a", "b"},
  			{"b", "cc"},
  			{"cc", "z"}
  		};
  
  		REQUIRE(utils::replace_all("a", rules) == "b");
  		REQUIRE(utils::replace_all("aaa a", rules) == "bbb b");
  		REQUIRE(utils::replace_all("abc", rules) == "bccc");
  		REQUIRE(utils::replace_all("ccab", rules) == "zbcc");
  	}
  
  	SECTION("from/to pairs can start with common substring") {
  		const std::vector<std::pair<std::string, std::string>> rules = {
  			{"%u", "lorem"},
  			{"%F", "ipsum"},
  			{"%%", "%"}
  		};
  
  		REQUIRE(utils::replace_all("%u", rules) == "lorem");
  		REQUIRE(utils::replace_all("%F", rules) == "ipsum");
  		REQUIRE(utils::replace_all("%%", rules) == "%");
  		REQUIRE(utils::replace_all("%% %u %F", rules) == "% lorem ipsum");
  	}
  
  	SECTION("if multiple pairs match, first one is used") {
  		const std::vector<std::pair<std::string, std::string>> rules = {
  			{"test", "lorem"},
  			{"t", "ipsum"},
  			{"tester", "dolar"}
  		};
  
  		REQUIRE(utils::replace_all("tester", rules) == "loremer");
  		REQUIRE(utils::replace_all("ts", rules) == "ipsums");
  	}
  }
  
  TEST_CASE("to_string()", "[utils]")
  {
  	REQUIRE(std::to_string(0) == "0");
  	REQUIRE(std::to_string(100) == "100");
  	REQUIRE(std::to_string(65536) == "65536");
  	REQUIRE(std::to_string(65537) == "65537");
  }
  
  TEST_CASE("partition_index()", "[utils]")
  {
  	std::vector<std::pair<unsigned int, unsigned int>> partitions;
  
  	SECTION("[0, 9] into 2") {
  		partitions = utils::partition_indexes(0, 9, 2);
  		REQUIRE(partitions.size() == 2);
  		REQUIRE(partitions[0].first == 0);
  		REQUIRE(partitions[0].second == 4);
  		REQUIRE(partitions[1].first == 5);
  		REQUIRE(partitions[1].second == 9);
  	}
  
  	SECTION("[0, 10] into 3") {
  		partitions = utils::partition_indexes(0, 10, 3);
  		REQUIRE(partitions.size() == 3);
  		REQUIRE(partitions[0].first == 0);
  		REQUIRE(partitions[0].second == 2);
  		REQUIRE(partitions[1].first == 3);
  		REQUIRE(partitions[1].second == 5);
  		REQUIRE(partitions[2].first == 6);
  		REQUIRE(partitions[2].second == 10);
  	}
  
  	SECTION("[0, 11] into 3") {
  		partitions = utils::partition_indexes(0, 11, 3);
  		REQUIRE(partitions.size() == 3);
  		REQUIRE(partitions[0].first == 0);
  		REQUIRE(partitions[0].second == 3);
  		REQUIRE(partitions[1].first == 4);
  		REQUIRE(partitions[1].second == 7);
  		REQUIRE(partitions[2].first == 8);
  		REQUIRE(partitions[2].second == 11);
  	}
  
  	SECTION("[0, 199] into 200") {
  		partitions = utils::partition_indexes(0, 199, 200);
  		REQUIRE(partitions.size() == 200);
  	}
  
  	SECTION("[0, 103] into 1") {
  		partitions = utils::partition_indexes(0, 103, 1);
  		REQUIRE(partitions.size() == 1);
  		REQUIRE(partitions[0].first == 0);
  		REQUIRE(partitions[0].second == 103);
  	}
  }
  
  TEST_CASE("censor_url()", "[utils]")
  {
  	REQUIRE(utils::censor_url("") == "");
  	REQUIRE(utils::censor_url("foobar") == "foobar");
  	REQUIRE(utils::censor_url("foobar://xyz/") == "foobar://xyz/");
  
  	REQUIRE(utils::censor_url("http://newsbeuter.org/") ==
  		"http://newsbeuter.org/");
  	REQUIRE(utils::censor_url("https://newsbeuter.org/") ==
  		"https://newsbeuter.org/");
  
  	REQUIRE(utils::censor_url("http://@newsbeuter.org/") ==
  		"http://newsbeuter.org/");
  	REQUIRE(utils::censor_url("https://@newsbeuter.org/") ==
  		"https://newsbeuter.org/");
  
  	REQUIRE(utils::censor_url("http://foo:bar@newsbeuter.org/") ==
  		"http://*:*@newsbeuter.org/");
  	REQUIRE(utils::censor_url("https://foo:bar@newsbeuter.org/") ==
  		"https://*:*@newsbeuter.org/");
  
  	REQUIRE(utils::censor_url("http://aschas@newsbeuter.org/") ==
  		"http://*:*@newsbeuter.org/");
  	REQUIRE(utils::censor_url("https://aschas@newsbeuter.org/") ==
  		"https://*:*@newsbeuter.org/");
  
  	REQUIRE(utils::censor_url("xxx://aschas@newsbeuter.org/") ==
  		"xxx://*:*@newsbeuter.org/");
  
  	REQUIRE(utils::censor_url("http://foobar") == "http://foobar/");
  	REQUIRE(utils::censor_url("https://foobar") == "https://foobar/");
  
  	REQUIRE(utils::censor_url("http://aschas@host") == "http://*:*@host/");
  	REQUIRE(utils::censor_url("https://aschas@host") == "https://*:*@host/");
  
  	REQUIRE(utils::censor_url("query:name:age between 1:10") ==
  		"query:name:age between 1:10");
  }
  
  TEST_CASE("absolute_url()", "[utils]")
  {
  	REQUIRE(utils::absolute_url("http://foobar/hello/crook/", "bar.html") ==
  		"http://foobar/hello/crook/bar.html");
  	REQUIRE(utils::absolute_url("https://foobar/foo/", "/bar.html") ==
  		"https://foobar/bar.html");
  	REQUIRE(utils::absolute_url("https://foobar/foo/",
  			"http://quux/bar.html") == "http://quux/bar.html");
  	REQUIRE(utils::absolute_url("http://foobar", "bla.html") ==
  		"http://foobar/bla.html");
  	REQUIRE(utils::absolute_url("http://test:test@foobar:33",
  			"bla2.html") == "http://test:test@foobar:33/bla2.html");
  }
  
  TEST_CASE("absolute_url() strips ASCII whitespace", "[utils]")
  {
  	// ASCII whitespace characters are: tab, line feed, form feed, carriage
  	// return, and space
  
  	REQUIRE(utils::absolute_url(" \t https://example.com/page1",
  			"page2") == "https://example.com/page2");
  	REQUIRE(utils::absolute_url("https://example.com/page3  \n ",
  			"page4") == "https://example.com/page4");
  	REQUIRE(utils::absolute_url("  \rhttps://example.com/page5  \f ",
  			"page6") == "https://example.com/page6");
  	REQUIRE(utils::absolute_url("https://example.com/base",
  			" \n replacement") == "https://example.com/replacement");
  	REQUIRE(utils::absolute_url("https://example.com/different",
  			"   another\t") == "https://example.com/another");
  	REQUIRE(utils::absolute_url("https://example.com/~joe/",
  			" \rhello\f") == "https://example.com/~joe/hello");
  	REQUIRE(utils::absolute_url( "\f\n\rhttps://example.com/misc/\t    ",
  			"   \t\feverything_at_once\n\r") == "https://example.com/misc/everything_at_once");
  }
  
  TEST_CASE("quote_for_stfl() adds a \'>\' after every \'<\'", "[utils]")
  {
  	REQUIRE(utils::quote_for_stfl("<<><><><") == "<><>><>><>><>");
  	REQUIRE(utils::quote_for_stfl("test") == "test");
  }
  
  TEST_CASE("quote()", "[utils]")
  {
  	REQUIRE(utils::quote("") == "\"\"");
  	REQUIRE(utils::quote("hello world") == "\"hello world\"");
  	REQUIRE(utils::quote("\"hello world\"") == "\"\\\"hello world\\\"\"");
  }
  
  TEST_CASE("to_u()", "[utils]")
  {
  	REQUIRE(utils::to_u("0") == 0);
  	REQUIRE(utils::to_u("23") == 23);
  	REQUIRE(utils::to_u("") == 0);
  }
  
  TEST_CASE("strwidth()", "[utils]")
  {
  	REQUIRE(utils::strwidth("") == 0);
  
  	REQUIRE(utils::strwidth("xx") == 2);
  
  	const auto input1 = utils::wstr2str(L"\uF91F");
  	REQUIRE(utils::strwidth(input1) == 2);
  	REQUIRE(utils::strwidth("\07") == 0);
  }
  
  TEST_CASE("strwidth_stfl()", "[utils]")
  {
  	REQUIRE(utils::strwidth_stfl("") == 0);
  
  	REQUIRE(utils::strwidth_stfl("x<hi>x") == 2);
  
  	REQUIRE(utils::strwidth_stfl("x<longtag>x</>") == 2);
  
  	REQUIRE(utils::strwidth_stfl("x<>x") == 3);
  
  	REQUIRE(utils::strwidth_stfl("x<>hi>x") == 6);
  
  	REQUIRE(utils::strwidth_stfl("x<>y<>z") == 5);
  
  	const auto input1 = utils::wstr2str(L"\uF91F");
  	REQUIRE(utils::strwidth_stfl(input1) == 2);
  	REQUIRE(utils::strwidth_stfl("\07") == 0);
  
  	REQUIRE(utils::strwidth_stfl("<a") == 0); // #415
  	REQUIRE(utils::strwidth_stfl("a") == 1);
  	REQUIRE(utils::strwidth_stfl("abc<tag>def") == 6);
  	REQUIRE(utils::strwidth_stfl("less-than: <>") ==  12);
  	REQUIRE(utils::strwidth_stfl("ＡＢＣＤＥＦ") == 12);
  }
  
  TEST_CASE("is_http_url()", "[utils]")
  {
  	REQUIRE(utils::is_http_url("http://example.com"));
  	REQUIRE(utils::is_http_url("http://foo.bar"));
  	REQUIRE(utils::is_http_url("https://foo.bar"));
  	REQUIRE(utils::is_http_url("http://"));
  	REQUIRE(utils::is_http_url("https://"));
  
  	REQUIRE_FALSE(utils::is_http_url("htt://foo.bar"));
  	REQUIRE_FALSE(utils::is_http_url("http:/"));
  	REQUIRE_FALSE(utils::is_http_url("foo://bar"));
  }
  
  TEST_CASE("join()", "[utils]")
  {
  	std::vector<std::string> str;
  	REQUIRE(utils::join(str, "") == "");
  	REQUIRE(utils::join(str, "-") == "");
  
  	SECTION("Join of one element") {
  		str.push_back("foobar");
  		REQUIRE(utils::join(str, "") == "foobar");
  		REQUIRE(utils::join(str, "-") == "foobar");
  
  		SECTION("Join of two elements") {
  			str.push_back("quux");
  			REQUIRE(utils::join(str, "") == "foobarquux");
  			REQUIRE(utils::join(str, "-") == "foobar-quux");
  		}
  	}
  }
  
  TEST_CASE("trim()", "[utils]")
  {
  	std::string str = "  xxx\r\n";
  	utils::trim(str);
  	REQUIRE(str == "xxx");
  
  	str = "\n\n abc  foobar\n";
  	utils::trim(str);
  	REQUIRE(str == "abc  foobar");
  
  	str = "";
  	utils::trim(str);
  	REQUIRE(str == "");
  
  	str = "     \n";
  	utils::trim(str);
  	REQUIRE(str == "");
  }
  
  TEST_CASE("trim_end()", "[utils]")
  {
  	std::string str = "quux\n";
  	utils::trim_end(str);
  	REQUIRE(str == "quux");
  }
  
  TEST_CASE("utils::make_title extracts possible title from URL", "[utils]")
  {
  	SECTION("Uses last part of URL as title") {
  		auto input = "http://example.com/Item";
  		REQUIRE(utils::make_title(input) == "Item");
  	}
  
  	SECTION("Replaces dashes and underscores with spaces") {
  		std::string input;
  
  		SECTION("Dashes") {
  			input = "http://example.com/This-is-the-title";
  		}
  
  		SECTION("Underscores") {
  			input = "http://example.com/This_is_the_title";
  		}
  
  		SECTION("Mix of dashes and underscores") {
  			input = "http://example.com/This_is-the_title";
  		}
  
  		SECTION("Eliminate .php extension") {
  			input = "http://example.com/This_is-the_title.php";
  		}
  
  		SECTION("Eliminate .html extension") {
  			input = "http://example.com/This_is-the_title.html";
  		}
  
  		SECTION("Eliminate .htm extension") {
  			input = "http://example.com/This_is-the_title.htm";
  		}
  
  		SECTION("Eliminate .aspx extension") {
  			input = "http://example.com/This_is-the_title.aspx";
  		}
  
  		REQUIRE(utils::make_title(input) == "This is the title");
  	}
  
  	SECTION("Capitalizes first letter of extracted title") {
  		auto input = "http://example.com/this-is-the-title";
  		REQUIRE(utils::make_title(input) == "This is the title");
  	}
  
  	SECTION("Only cares about last component of the URL") {
  		auto input = "http://example.com/items/misc/this-is-the-title";
  		REQUIRE(utils::make_title(input) == "This is the title");
  	}
  
  	SECTION("Strips out trailing slashes") {
  		std::string input;
  
  		SECTION("One slash") {
  			input = "http://example.com/item/";
  		}
  
  		SECTION("Numerous slashes") {
  			input = "http://example.com/item/////////////";
  		}
  
  		REQUIRE(utils::make_title(input) == "Item");
  	}
  
  	SECTION("Doesn't mind invalid URL scheme") {
  		auto input = "blahscheme://example.com/this-is-the-title";
  		REQUIRE(utils::make_title(input) == "This is the title");
  	}
  
  	SECTION("Strips out URL query parameters") {
  		SECTION("Single parameter") {
  			auto input =
  				"http://example.com/story/aug/"
  				"title-with-dashes?a=b";
  			REQUIRE(utils::make_title(input) ==
  				"Title with dashes");
  		}
  
  		SECTION("Multiple parameters") {
  			auto input =
  				"http://example.com/"
  				"title-with-dashes?a=b&x=y&utf8=✓";
  			REQUIRE(utils::make_title(input) ==
  				"Title with dashes");
  		}
  	}
  
  	SECTION("Decodes percent-encoded characters") {
  		auto input = "https://example.com/It%27s%202017%21";
  		REQUIRE(utils::make_title(input) == "It's 2017!");
  	}
  
  	SECTION("Deal with an empty last component") {
  		auto input = "https://example.com/?format=rss";
  		REQUIRE(utils::make_title(input) == "");
  	}
  
  	SECTION("Deal with an empty input") {
  		auto input = "";
  		REQUIRE(utils::make_title(input) == "");
  	}
  }
  
  TEST_CASE("run_interactively runs a command with inherited I/O", "[utils]")
  {
  	SECTION("echo hello should return 0") {
  		const auto result = utils::run_interactively("echo hello", "test");
  		REQUIRE(result == 0);
  	}
  	SECTION("exit 1 should return 1") {
  		const auto result = utils::run_interactively("exit 1", "test");
  		REQUIRE(result == 1);
  	}
  
  	// Unfortunately, there is no easy way to provoke this function to return
  	// `nonstd::nullopt`, nor to test that it returns just the lower 8 bits.
  }
  
  TEST_CASE("run_non_interactively runs a command without I/O", "[utils]")
  {
  	SECTION("echo hello should return 0") {
  		const auto result = utils::run_non_interactively("echo hello", "test");
  		REQUIRE(result == 0);
  	}
  	SECTION("exit 1 should return 1") {
  		const auto result = utils::run_non_interactively("exit 1", "test");
  		REQUIRE(result == 1);
  	}
  
  	// Unfortunately, there is no easy way to provoke this function to return
  	// `nonstd::nullopt`, nor to test that it returns just the lower 8 bits.
  }
  
  TEST_CASE("run_non_interactively waits for program to finish", "[utils]")
  {
  	using namespace std::chrono;
  
  	SECTION("when sleeping, we use at least as much time as specified") {
  		const auto start = steady_clock::now();
  
  		const auto result = utils::run_non_interactively("sleep 1", "test");
  		REQUIRE(result == 0);
  
  		const auto finish = steady_clock::now();
  		const auto runtime = duration_cast<milliseconds>(finish - start);
  		REQUIRE(runtime.count() >= 1000);
  	}
  
  	SECTION("if program is put in background, using &, we return immediately") {
  		const auto start = steady_clock::now();
  
  		const auto result = utils::run_non_interactively("sleep 5 &", "test");
  		REQUIRE(result == 0);
  
  		const auto finish = steady_clock::now();
  		const auto runtime = duration_cast<milliseconds>(finish - start);
  		REQUIRE(runtime.count() < 5000);
  	}
  }
  
  TEST_CASE("remove_soft_hyphens remove all U+00AD characters from a string",
  	"[utils]")
  {
  	SECTION("doesn't do anything if input has no soft hyphens in it") {
  		std::string data = "hello world!";
  		REQUIRE_NOTHROW(utils::remove_soft_hyphens(data));
  		REQUIRE(data == "hello world!");
  	}
  
  	SECTION("removes *all* soft hyphens") {
  		std::string data = "hy\u00ADphen\u00ADa\u00ADtion";
  		REQUIRE_NOTHROW(utils::remove_soft_hyphens(data));
  		REQUIRE(data == "hyphenation");
  	}
  
  	SECTION("removes consequtive soft hyphens") {
  		std::string data =
  			"don't know why any\u00AD\u00ADone would do that";
  		REQUIRE_NOTHROW(utils::remove_soft_hyphens(data));
  		REQUIRE(data == "don't know why anyone would do that");
  	}
  
  	SECTION("removes soft hyphen at the beginning of the line") {
  		std::string data = "\u00ADtion";
  		REQUIRE_NOTHROW(utils::remove_soft_hyphens(data));
  		REQUIRE(data == "tion");
  	}
  
  	SECTION("removes soft hyphen at the end of the line") {
  		std::string data = "over\u00AD";
  		REQUIRE_NOTHROW(utils::remove_soft_hyphens(data));
  		REQUIRE(data == "over");
  	}
  }
  
  TEST_CASE(
  	"substr_with_width() returns a longest substring fits to the given "
  	"width",
  	"[utils]")
  {
  	REQUIRE(utils::substr_with_width("a", 1) == "a");
  	REQUIRE(utils::substr_with_width("a", 2) == "a");
  	REQUIRE(utils::substr_with_width("ab", 1) == "a");
  	REQUIRE(utils::substr_with_width("abc", 1) == "a");
  	REQUIRE(utils::substr_with_width("A\u3042B\u3044C\u3046", 5) ==
  		"A\u3042B");
  
  	SECTION("returns an empty string if the given string is empty") {
  		REQUIRE(utils::substr_with_width("", 0) == "");
  		REQUIRE(utils::substr_with_width("", 1) == "");
  	}
  
  	SECTION("returns an empty string if the given width is zero") {
  		REQUIRE(utils::substr_with_width("world", 0) == "");
  		REQUIRE(utils::substr_with_width("", 0) == "");
  	}
  
  	SECTION("doesn't split single codepoint in two") {
  		std::string data = "\u3042\u3044\u3046";
  		REQUIRE(utils::substr_with_width(data, 1) == "");
  		REQUIRE(utils::substr_with_width(data, 3) == "\u3042");
  		REQUIRE(utils::substr_with_width(data, 5) == "\u3042\u3044");
  	}
  
  	SECTION("handles angular brackets as regular characters") {
  		REQUIRE(utils::substr_with_width("ＡＢＣ<b>ＤＥ</b>Ｆ", 9) ==
  			"ＡＢＣ<b>");
  		REQUIRE(utils::substr_with_width("<foobar>ＡＢＣ", 4) ==
  			"<foo");
  		REQUIRE(utils::substr_with_width("a<<xyz>>bcd", 3) ==
  			"a<<");
  		REQUIRE(utils::substr_with_width("ＡＢＣ<b>ＤＥ", 10) ==
  			"ＡＢＣ<b>");
  		REQUIRE(utils::substr_with_width("a</>b</>c</>", 2) ==
  			"a<");
  		REQUIRE(utils::substr_with_width("<><><>", 2) == "<>");
  		REQUIRE(utils::substr_with_width("a<>b<>c", 3) == "a<>");
  	}
  
  	SECTION("treat non-printable has zero width") {
  		REQUIRE(utils::substr_with_width("\x01\x02"
  				"abc",
  				1) ==
  			"\x01\x02"
  			"a");
  	}
  }
  
  TEST_CASE(
  	"substr_with_width_stfl() returns a longest substring fits to the given "
  	"width",
  	"[utils]")
  {
  	REQUIRE(utils::substr_with_width_stfl("a", 1) == "a");
  	REQUIRE(utils::substr_with_width_stfl("a", 2) == "a");
  	REQUIRE(utils::substr_with_width_stfl("ab", 1) == "a");
  	REQUIRE(utils::substr_with_width_stfl("abc", 1) == "a");
  	REQUIRE(utils::substr_with_width_stfl("A\u3042B\u3044C\u3046", 5) ==
  		"A\u3042B");
  
  	SECTION("returns an empty string if the given string is empty") {
  		REQUIRE(utils::substr_with_width_stfl("", 0) == "");
  		REQUIRE(utils::substr_with_width_stfl("", 1) == "");
  	}
  
  	SECTION("returns an empty string if the given width is zero") {
  		REQUIRE(utils::substr_with_width_stfl("world", 0) == "");
  		REQUIRE(utils::substr_with_width_stfl("", 0) == "");
  	}
  
  	SECTION("doesn't split single codepoint in two") {
  		std::string data = "\u3042\u3044\u3046";
  		REQUIRE(utils::substr_with_width_stfl(data, 1) == "");
  		REQUIRE(utils::substr_with_width_stfl(data, 3) == "\u3042");
  		REQUIRE(utils::substr_with_width_stfl(data, 5) == "\u3042\u3044");
  	}
  
  	SECTION("doesn't count a width of STFL tag") {
  		REQUIRE(utils::substr_with_width_stfl("ＡＢＣ<b>ＤＥ</b>Ｆ", 9) ==
  			"ＡＢＣ<b>Ｄ");
  		REQUIRE(utils::substr_with_width_stfl("<foobar>ＡＢＣ", 4) ==
  			"<foobar>ＡＢ");
  		REQUIRE(utils::substr_with_width_stfl("a<<xyz>>bcd", 3) ==
  			"a<<xyz>>b"); // tag: "<<xyz>"
  		REQUIRE(utils::substr_with_width_stfl("ＡＢＣ<b>ＤＥ", 10) ==
  			"ＡＢＣ<b>ＤＥ");
  		REQUIRE(utils::substr_with_width_stfl("a</>b</>c</>", 2) ==
  			"a</>b</>");
  	}
  
  	SECTION("count a width of escaped less-than mark") {
  		REQUIRE(utils::substr_with_width_stfl("<><><>", 2) == "<><>");
  		REQUIRE(utils::substr_with_width_stfl("a<>b<>c", 3) == "a<>b");
  	}
  
  	SECTION("treat non-printable has zero width") {
  		REQUIRE(utils::substr_with_width_stfl("\x01\x02"
  				"abc",
  				1) ==
  			"\x01\x02"
  			"a");
  	}
  }
  
  TEST_CASE("getcwd() returns current directory of the process", "[utils]")
  {
  	SECTION("Returns non-empty string") {
  		REQUIRE(utils::getcwd().length() > 0);
  	}
  
  	SECTION("Value depends on current directory") {
  		const std::string maindir = utils::getcwd();
  		// Other tests already rely on the presense of "data" directory
  		// next to the executable, so it's okay to use that dependency
  		// here, too
  		const std::string subdir = "data";
  		REQUIRE(0 == ::chdir(subdir.c_str()));
  		const std::string datadir = utils::getcwd();
  		REQUIRE(0 == ::chdir(".."));
  		const std::string backdir = utils::getcwd();
  
  		INFO("maindir = " << maindir);
  		INFO("backdir = " << backdir);
  
  		REQUIRE(maindir == backdir);
  		REQUIRE_FALSE(maindir == datadir);
  
  		// Datadir path starts with path to maindir
  		REQUIRE(datadir.find(maindir) == 0);
  		// Datadir path ends with "data" string
  		REQUIRE(datadir.substr(datadir.length() - subdir.length()) ==
  			subdir);
  	}
  
  	SECTION("Returns empty string if current directory doesn't exist") {
  		// Create a temporary directory, change to it and delete it. This leads
  		// getcwd to fail.
  		TestHelpers::TempDir tempdir;
  
  		const std::string tempdir_path = tempdir.get_path();
  		INFO("tempdir = " << tempdir_path);
  
  		TestHelpers::Chdir chdir(tempdir_path);
  
  		REQUIRE(0 == ::rmdir(tempdir_path.c_str()));
  
  		REQUIRE("" == utils::getcwd());
  	}
  }
  
  TEST_CASE("read_text_file() returns file contents line by line", "[utils]")
  {
  	TestHelpers::TempFile tempfile;
  
  	SECTION("succesful if test file contains only valid UTF-8") {
  		{
  			std::ofstream f(tempfile.get_path());
  			f << "lorem ipsum\ntest1\ntest2";
  		}
  
  		const auto result = utils::read_text_file(tempfile.get_path());
  		REQUIRE(result);
  		const auto content = result.value();
  		REQUIRE(content.size() == 3);
  		REQUIRE(content[0] == "lorem ipsum");
  		REQUIRE(content[1] == "test1");
  		REQUIRE(content[2] == "test2");
  	}
  
  	SECTION("fails with error message if file contains invalid UTF-8") {
  		{
  			std::ofstream f(tempfile.get_path());
  			f << "test1\nt\xffst2"; // \xff is an invalid UTF-8 codepoint
  		}
  		std::vector<std::string> content;
  		std::string error_message;
  
  		const auto result = utils::read_text_file(tempfile.get_path());
  		REQUIRE_FALSE(result);
  		REQUIRE(result.error().message.size() > 0);
  	}
  }
  
  TEST_CASE("strnaturalcmp() compares strings using natural numeric ordering",
  	"[utils]")
  {
  	// Tests copied over from 3rd-party/alphanum.hpp
  	REQUIRE(utils::strnaturalcmp("","") == 0);
  	REQUIRE(utils::strnaturalcmp("","a") < 0);
  	REQUIRE(utils::strnaturalcmp("a","") > 0);
  	REQUIRE(utils::strnaturalcmp("a","a") == 0);
  	REQUIRE(utils::strnaturalcmp("","9") < 0);
  	REQUIRE(utils::strnaturalcmp("9","") > 0);
  	REQUIRE(utils::strnaturalcmp("1","1") == 0);
  	REQUIRE(utils::strnaturalcmp("1","2") < 0);
  	REQUIRE(utils::strnaturalcmp("3","2") > 0);
  	REQUIRE(utils::strnaturalcmp("a1","a1") == 0);
  	REQUIRE(utils::strnaturalcmp("a1","a2") < 0);
  	REQUIRE(utils::strnaturalcmp("a2","a1") > 0);
  	REQUIRE(utils::strnaturalcmp("a1a2","a1a3") < 0);
  	REQUIRE(utils::strnaturalcmp("a1a2","a1a0") > 0);
  	REQUIRE(utils::strnaturalcmp("134","122") > 0);
  	REQUIRE(utils::strnaturalcmp("12a3","12a3") == 0);
  	REQUIRE(utils::strnaturalcmp("12a1","12a0") > 0);
  	REQUIRE(utils::strnaturalcmp("12a1","12a2") < 0);
  	REQUIRE(utils::strnaturalcmp("a","aa") < 0);
  	REQUIRE(utils::strnaturalcmp("aaa","aa") > 0);
  	REQUIRE(utils::strnaturalcmp("Alpha 2","Alpha 2") == 0);
  	REQUIRE(utils::strnaturalcmp("Alpha 2","Alpha 2A") < 0);
  	REQUIRE(utils::strnaturalcmp("Alpha 2 B","Alpha 2") > 0);
  
  	REQUIRE(utils::strnaturalcmp("aa10", "aa2") > 0);
  }
  
  TEST_CASE(
  	"is_valid_podcast_type() returns true if supplied MIME type "
  	"is audio, video, or a container",
  	"[utils]")
  {
  	REQUIRE(utils::is_valid_podcast_type("audio/mpeg"));
  	REQUIRE(utils::is_valid_podcast_type("audio/mp3"));
  	REQUIRE(utils::is_valid_podcast_type("audio/x-mp3"));
  	REQUIRE(utils::is_valid_podcast_type("audio/ogg"));
  	REQUIRE(utils::is_valid_podcast_type("video/x-matroska"));
  	REQUIRE(utils::is_valid_podcast_type("video/webm"));
  	REQUIRE(utils::is_valid_podcast_type("application/ogg"));
  
  	REQUIRE_FALSE(utils::is_valid_podcast_type("image/jpeg"));
  	REQUIRE_FALSE(utils::is_valid_podcast_type("image/png"));
  	REQUIRE_FALSE(utils::is_valid_podcast_type("text/plain"));
  	REQUIRE_FALSE(utils::is_valid_podcast_type("application/zip"));
  }
  
  TEST_CASE("podcast_mime_to_link_type() returns HtmlRenderer's LinkType that "
  	"corresponds to the given podcast MIME type",
  	"[utils]")
  {
  	SECTION("Valid podcast MIME types") {
  		const auto check = [](std::string mime, LinkType expected) {
  			REQUIRE(utils::podcast_mime_to_link_type(mime) == expected);
  		};
  
  		check("audio/mpeg", LinkType::AUDIO);
  		check("audio/mp3", LinkType::AUDIO);
  		check("audio/x-mp3", LinkType::AUDIO);
  		check("audio/ogg", LinkType::AUDIO);
  		check("video/x-matroska", LinkType::VIDEO);
  		check("video/webm", LinkType::VIDEO);
  		check("application/ogg", LinkType::AUDIO);
  	}
  
  	SECTION("Sets `ok` to `false` if given MIME type is not a podcast type") {
  		const auto check = [](std::string mime) {
  			REQUIRE(utils::podcast_mime_to_link_type(mime) == nonstd::nullopt);
  		};
  
  		check("image/jpeg");
  		check("image/png");
  		check("text/plain");
  		check("application/zip");
  	}
  }
  
  TEST_CASE(
  	"is_valid_color() returns false for things that aren't valid STFL "
  	"colors",
  	"[utils]")
  {
  	const std::vector<std::string> non_colors{
  		"awesome", "list", "of", "things", "that", "aren't", "colors"};
  	for (const auto& input : non_colors) {
  		REQUIRE_FALSE(utils::is_valid_color(input));
  	}
  }
  
  TEST_CASE(
  	"is_query_url() returns true if given URL is a query URL, i.e. it "
  	"starts with \"query:\" string",
  	"[utils]")
  {
  	REQUIRE(utils::is_query_url("query:"));
  	REQUIRE(utils::is_query_url("query: example"));
  	REQUIRE_FALSE(utils::is_query_url("query"));
  	REQUIRE_FALSE(utils::is_query_url("   query:"));
  }
  
  TEST_CASE(
  	"is_filter_url() returns true if given URL is a filter URL, i.e. it "
  	"starts with \"filter:\" string",
  	"[utils]")
  {
  	REQUIRE(utils::is_filter_url("filter:"));
  	REQUIRE(utils::is_filter_url("filter: example"));
  	REQUIRE_FALSE(utils::is_filter_url("filter"));
  	REQUIRE_FALSE(utils::is_filter_url("   filter:"));
  }
  
  TEST_CASE(
  	"is_exec_url() returns true if given URL is a exec URL, i.e. it "
  	"starts with \"exec:\" string",
  	"[utils]")
  {
  	REQUIRE(utils::is_exec_url("exec:"));
  	REQUIRE(utils::is_exec_url("exec: example"));
  	REQUIRE_FALSE(utils::is_exec_url("exec"));
  	REQUIRE_FALSE(utils::is_exec_url("   exec:"));
  }
  
  TEST_CASE(
  	"is_special_url() return true if given URL is a query, filter or exec "
  	"URL, i.e. it "
  	"starts with \"query:\", \"filter:\" or \"exec:\" string",
  	"[utils]")
  {
  	REQUIRE(utils::is_special_url("query:"));
  	REQUIRE(utils::is_special_url("query: example"));
  	REQUIRE_FALSE(utils::is_special_url("query"));
  	REQUIRE_FALSE(utils::is_special_url("   query:"));
  
  	REQUIRE(utils::is_special_url("filter:"));
  	REQUIRE(utils::is_special_url("filter: example"));
  	REQUIRE_FALSE(utils::is_special_url("filter"));
  	REQUIRE_FALSE(utils::is_special_url("   filter:"));
  
  	REQUIRE(utils::is_special_url("exec:"));
  	REQUIRE(utils::is_special_url("exec: example"));
  	REQUIRE_FALSE(utils::is_special_url("exec"));
  	REQUIRE_FALSE(utils::is_special_url("   exec:"));
  }
  
  TEST_CASE(
  	"get_default_browser() returns BROWSER environment variable or "
  	"\"lynx\" if the variable is not set",
  	"[utils]")
  {
  	TestHelpers::EnvVar browserEnv("BROWSER");
  
  	// If BROWSER is not set, default browser is lynx(1)
  	browserEnv.unset();
  	REQUIRE(utils::get_default_browser() == "lynx");
  
  	browserEnv.set("firefox");
  	REQUIRE(utils::get_default_browser() == "firefox");
  
  	browserEnv.set("opera");
  	REQUIRE(utils::get_default_browser() == "opera");
  }
  
  TEST_CASE(
  	"get_basename() returns basename from a URL if available, if not"
  	" then an empty string",
  	"[utils]")
  {
  	SECTION("return basename in the presence of GET parameters") {
  		REQUIRE(utils::get_basename("https://example.org/path/to/file.mp3?param=value#fragment")
  			== "file.mp3");
  		REQUIRE(utils::get_basename("https://example.org/file.mp3") == "file.mp3");
  	}
  
  	SECTION("return empty string when basename is unavailable") {
  		REQUIRE(utils::get_basename("https://example.org/?param=value#fragment")
  			== "");
  		REQUIRE(utils::get_basename("https://example.org/path/to/?param=value#fragment")
  			== "");
  	}
  }
  
  TEST_CASE(
  	"get_auth_method() returns enumerated constant "
  	"on defined values and undefined values",
  	"[utils]")
  {
  	REQUIRE(utils::get_auth_method("any") == CURLAUTH_ANY);
  	REQUIRE(utils::get_auth_method("ntlm") == CURLAUTH_NTLM);
  	REQUIRE(utils::get_auth_method("basic") == CURLAUTH_BASIC);
  	REQUIRE(utils::get_auth_method("digest") == CURLAUTH_DIGEST);
  	REQUIRE(utils::get_auth_method("digest_ie") == CURLAUTH_DIGEST_IE);
  	REQUIRE(utils::get_auth_method("gssnegotiate") == CURLAUTH_GSSNEGOTIATE);
  	REQUIRE(utils::get_auth_method("anysafe") == CURLAUTH_ANYSAFE);
  
  	REQUIRE(utils::get_auth_method("") == CURLAUTH_ANY);
  	REQUIRE(utils::get_auth_method("test") == CURLAUTH_ANY);
  }
  
  TEST_CASE(
  	"get_proxy_type() returns enumerated constant "
  	"on defined values and undefined values",
  	"[utils]")
  {
  	REQUIRE(utils::get_proxy_type("http") == CURLPROXY_HTTP);
  	REQUIRE(utils::get_proxy_type("socks4") == CURLPROXY_SOCKS4);
  	REQUIRE(utils::get_proxy_type("socks5") == CURLPROXY_SOCKS5);
  	REQUIRE(utils::get_proxy_type("socks5h") == CURLPROXY_SOCKS5_HOSTNAME);
  
  	REQUIRE(utils::get_proxy_type("") == CURLPROXY_HTTP);
  	REQUIRE(utils::get_proxy_type("test") == CURLPROXY_HTTP);
  }
  
  TEST_CASE("is_valid_attribute returns true if given string is an STFL attribute",
  	"[utils]")
  {
  	const std::vector<std::string> invalid = {
  		"foo",
  		"bar",
  		"baz",
  		"quux"
  	};
  	for (const auto& attr : invalid) {
  		REQUIRE_FALSE(utils::is_valid_attribute(attr));
  	}
  
  	const std::vector<std::string> valid = {
  		"standout",
  		"underline",
  		"reverse",
  		"blink",
  		"dim",
  		"bold",
  		"protect",
  		"invis",
  		"default"
  	};
  	for (const auto& attr : valid) {
  		REQUIRE(utils::is_valid_attribute(attr));
  	}
  }
  
  TEST_CASE("unescape_url() takes a percent-encoded string and returns the string "
  	"with a precent escaped string",
  	"[utils]")
  {
  	REQUIRE(utils::unescape_url("foo%20bar") == "foo bar");
  	REQUIRE(utils::unescape_url(
  			"%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D") ==
  		"!#$&'()*+,/:;=?@[]");
  	REQUIRE(utils::unescape_url("%00") == "");
  
  }
  
  TEST_CASE("gentabs() calculates padding tabs based on stringwidth", "[utils]")
  {
  	REQUIRE(utils::gentabs("") == 4);
  	REQUIRE(utils::gentabs("aaaaaaa") == 4);
  	REQUIRE(utils::gentabs("aaaaaaaa") == 3);
  	REQUIRE(utils::gentabs("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") == 1);
  }
  
  TEST_CASE("mkdir_parents() creates all paths components and returns 0 if "
  	"the path now exists",
  	"[utils]")
  {
  	TestHelpers::TempDir tmp;
  
  	const auto require_return_zero = [](const std::string& path) {
  		REQUIRE(utils::mkdir_parents(path, 0700) == 0);
  		REQUIRE(::access(path.c_str(), R_OK | X_OK) == 0);
  	};
  
  	SECTION("Simple test on temporary dir itself") {
  		const auto path = tmp.get_path();
  		INFO("Path is " << path);
  		require_return_zero(path);
  	}
  
  	SECTION("Zero intermediate directories") {
  		const auto path = tmp.get_path() + std::to_string(rand());
  		INFO("Path is " << path);
  
  		SECTION("Target doesn't yet exist") {
  			require_return_zero(path);
  		}
  
  		SECTION("Target already exists") {
  			REQUIRE(::mkdir(path.c_str(), 0700) == 0);
  			require_return_zero(path);
  		}
  	}
  
  	SECTION("One intermediate directory") {
  		const auto intermediate_path = tmp.get_path() + std::to_string(rand());
  		const auto path = intermediate_path + "/" + std::to_string(rand());
  		INFO("Path is " << path);
  
  		SECTION("Which doesn't exist") {
  			require_return_zero(path);
  		}
  
  		SECTION("Which exists") {
  			REQUIRE(::mkdir(intermediate_path.c_str(), 0700) == 0);
  
  			SECTION("Target doesn't exist") {
  				require_return_zero(path);
  			}
  
  			SECTION("Target exists") {
  				REQUIRE(::mkdir(path.c_str(), 0700) == 0);
  				require_return_zero(path);
  			}
  		}
  	}
  
  	SECTION("Two intermediate directories") {
  		const auto intermediate_path1 = tmp.get_path() + std::to_string(rand());
  		const auto intermediate_path2 =
  			intermediate_path1 + "/" + std::to_string(rand());
  		const auto path = intermediate_path2 + "/" + std::to_string(rand());
  		INFO("Path is " << path);
  
  		SECTION("Which don't exist") {
  			require_return_zero(path);
  		}
  
  		SECTION("First one exists") {
  			REQUIRE(::mkdir(intermediate_path1.c_str(), 0700) == 0);
  
  			SECTION("Second one exists") {
  				REQUIRE(::mkdir(intermediate_path2.c_str(), 0700) == 0);
  
  				SECTION("Target exists") {
  					REQUIRE(::mkdir(path.c_str(), 0700) == 0);
  					require_return_zero(path);
  				}
  
  				SECTION("Target doesn't exist") {
  					require_return_zero(path);
  				}
  			}
  
  			SECTION("Second one doesn't exist") {
  				require_return_zero(path);
  			}
  		}
  	}
  }
  
  TEST_CASE("mkdir_parents() doesn't care if the path ends in a slash or not",
  	"[utils]")
  {
  	TestHelpers::TempDir tmp;
  
  	const auto path = tmp.get_path() + std::to_string(rand());
  
  	const auto check = [](const std::string& path) {
  		REQUIRE(utils::mkdir_parents(path, 0700) == 0);
  		REQUIRE(::access(path.c_str(), R_OK | X_OK) == 0);
  	};
  
  	SECTION("Path doesn't end in slash => directory created") {
  		check(path);
  	}
  
  	SECTION("Path ends in slash => directory created") {
  		check(path + "/");
  	}
  }
  
  TEST_CASE("utf8_to_locale() converts text from UTF-8 to the encoding specified "
  	"by locale in LC_CTYPE class",
  	"[utils]")
  {
  	TestHelpers::LcCtypeEnvVar lc_ctype;
  	const auto set_locale = [&lc_ctype](std::string new_locale) -> bool {
  		if (::setlocale(LC_CTYPE, new_locale.c_str()) == nullptr)
  		{
  			WARN("Couldn't set locale " + new_locale + "; test skipped.");
  			return false;
  		}
  		lc_ctype.set(new_locale);
  		return true;
  	};
  
  	SECTION("UTF-8") {
  		if (!set_locale("en_US.UTF-8")) {
  			return;
  		}
  
  		REQUIRE(utils::utf8_to_locale("") == "");
  
  		// "Просто проверяю", "Just testing" in Russian.
  		const std::string text("\xd0\x9f\xd1\x80\xd0\xbe\xd1\x81\xd1\x82\xd0"
  			"\xbe\x20\xd0\xbf\xd1\x80\xd0\xbe\xd0\xb2\xd0\xb5\xd1\x80\xd1\x8f\xd1\x8e");
  		REQUIRE(utils::utf8_to_locale(text) == text);
  	}
  
  	SECTION("KOI8-R") {
  		if (!set_locale("ru_RU.KOI8-R")) {
  			return;
  		}
  
  		REQUIRE(utils::utf8_to_locale("") == "");
  
  		// "ещё один тест", "another test" in Russian.
  		const std::string input("\xd0\xb5\xd1\x89\xd1\x91\x20\xd0\xbe\xd0\xb4"
  			"\xd0\xb8\xd0\xbd\x20\xd1\x82\xd0\xb5\xd1\x81\xd1\x82");
  		const std::string expected("\xc5\xdd\xa3\x20\xcf\xc4\xc9\xce\x20\xd4"
  			"\xc5\xd3\xd4");
  		REQUIRE(utils::utf8_to_locale(input) == expected);
  	}
  
  	SECTION("CP1251") {
  		if (!set_locale("ru_RU.CP1251")) {
  			return;
  		}
  
  		REQUIRE(utils::utf8_to_locale("") == "");
  
  		// "Приветствую!", "Greetings!" in Russian.
  		const std::string input("\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1"
  			"\x82\xd1\x81\xd1\x82\xd0\xb2\xd1\x83\xd1\x8e\x21");
  		const std::string expected("\xcf\xf0\xe8\xe2\xe5\xf2\xf1\xf2\xe2\xf3"
  			"\xfe\x21");
  		REQUIRE(utils::utf8_to_locale(input) == expected);
  	}
  }
  
  TEST_CASE("utf8_to_locale() transliterates characters unsupported by the locale's encoding",
  	"[utils]")
  {
  	TestHelpers::LcCtypeEnvVar lc_ctype;
  	const auto set_locale = [&lc_ctype](std::string new_locale) -> bool {
  		if (::setlocale(LC_CTYPE, new_locale.c_str()) == nullptr)
  		{
  			WARN("Couldn't set locale " + new_locale + "; test skipped.");
  			return false;
  		}
  		lc_ctype.set(new_locale);
  		return true;
  	};
  
  	// Tests below don't spell out the expected result because different
  	// platforms might follow different transliteration rules. The best we can
  	// do is check that 1) the output is non-empty, i.e. there was no error in
  	// conversion; 2) the output is not the same as the input, i.e. the
  	// conversion did something.
  
  	SECTION("KOI8-R doesn't support Ukrainian characters") {
  		if (!set_locale("ru_RU.KOI8-R")) {
  			return;
  		}
  
  		// "Пісня", "Song" in Ukrainian.
  		const std::string input("\xd0\x9f\xd1\x96\xd1\x81\xd0\xbd\xd1\x8f");
  
  		const auto result = utils::utf8_to_locale(input);
  		REQUIRE(result != "");
  		REQUIRE(result != input);
  	}
  
  	SECTION("CP1251") {
  		if (!set_locale("ru_RU.CP1251")) {
  			return;
  		}
  
  		// "日本", "Japan" in Japanese
  		const std::string input("\xe6\x97\xa5\xe6\x9c\xac");
  
  		const auto result = utils::utf8_to_locale(input);
  		REQUIRE(result != "");
  		REQUIRE(result != input);
  	}
  }
  
  TEST_CASE("locale_to_utf8() converts text from the encoding specified by locale "
  	"in LC_CTYPE class to UTF-8",
  	"[utils]")
  {
  	TestHelpers::LcCtypeEnvVar lc_ctype;
  	const auto set_locale = [&lc_ctype](std::string new_locale) -> bool {
  		if (::setlocale(LC_CTYPE, new_locale.c_str()) == nullptr)
  		{
  			WARN("Couldn't set locale " + new_locale + "; test skipped.");
  			return false;
  		}
  		lc_ctype.set(new_locale);
  		return true;
  	};
  
  	SECTION("UTF-8") {
  		if (!set_locale("en_US.UTF-8")) {
  			return;
  		}
  
  		REQUIRE(utils::locale_to_utf8("") == "");
  
  		// "Newsboat мне нравится", "I like Newsboat" in Russian.
  		const std::string text("\x4e\x65\x77\x73\x62\x6f\x61\x74\x20\xd0\xbc"
  			"\xd0\xbd\xd0\xb5\x20\xd0\xbd\xd1\x80\xd0\xb0\xd0\xb2\xd0\xb8"
  			"\xd1\x82\xd1\x81\xd1\x8f");
  		REQUIRE(utils::locale_to_utf8(text) == text);
  	}
  
  	SECTION("KOI8-R") {
  		if (!set_locale("ru_RU.KOI8-R")) {
  			return;
  		}
  
  		REQUIRE(utils::locale_to_utf8("") == "");
  
  		// "великолепная проверка", "excellent check" in Russian.
  		const std::string input("\xd7\xc5\xcc\xc9\xcb\xcf\xcc\xc5\xd0\xce\xc1"
  			"\xd1\x20\xd0\xd2\xcf\xd7\xc5\xd2\xcb\xc1");
  		const std::string expected("\xd0\xb2\xd0\xb5\xd0\xbb\xd0\xb8\xd0\xba\xd0"
  			"\xbe\xd0\xbb\xd0\xb5\xd0\xbf\xd0\xbd\xd0\xb0\xd1\x8f\x20\xd0"
  			"\xbf\xd1\x80\xd0\xbe\xd0\xb2\xd0\xb5\xd1\x80\xd0\xba\xd0\xb0");
  		REQUIRE(utils::locale_to_utf8(input) == expected);
  	}
  
  	SECTION("CP1251") {
  		if (!set_locale("ru_RU.CP1251")) {
  			return;
  		}
  
  		REQUIRE(utils::locale_to_utf8("") == "");
  
  		// "Все тесты зелёные!", "All tests green!" in Russian.
  		const std::string input("\xc2\xf1\xe5\x20\xf2\xe5\xf1\xf2\xfb\x20\xe7"
  			"\xe5\xeb\xb8\xed\xfb\xe5\x21");
  		const std::string expected("\xd0\x92\xd1\x81\xd0\xb5\x20\xd1\x82\xd0\xb5"
  			"\xd1\x81\xd1\x82\xd1\x8b\x20\xd0\xb7\xd0\xb5\xd0\xbb\xd1\x91\xd0"
  			"\xbd\xd1\x8b\xd0\xb5\x21");
  		REQUIRE(utils::locale_to_utf8(input) == expected);
  	}
  }
  
  TEST_CASE("translit() returns the value of `tocode`, maybe with \"//TRANSLIT\" appended",
  	"[utils]")
  {
  	// The behaviour of translit() is inherently platform-dependent: some
  	// platforms support transliteration for some encodings, while others
  	// support it for different encodings, or don't support it at all. This
  	// tests checks just the bare minimum: the function doesn't crash, doesn't
  	// throw exceptions, and returns something resembling a correct value.
  
  	const auto check = [](std::string fromcode, std::string tocode) {
  		DYNAMIC_SECTION(fromcode << " → " << tocode) {
  			const auto expected1(tocode);
  			const auto expected2(tocode + "//TRANSLIT");
  
  			const auto actual = utils::translit(tocode, fromcode);
  
  			INFO("Expected " << expected1);
  			INFO("      or " << expected2);
  			INFO("But got: " << actual);
  
  			// Extra parentheses required by Catch2 -- otherwise it doesn't let us use operator||.
  			REQUIRE((actual == expected1 || actual == expected2));
  		}
  	};
  
  	check("ISO-8859-1", "UTF-8");
  	check("KOI8-R", "UTF-8");
  	check("UTF-16", "UTF-8");
  	check("UTF-8", "UTF-8");
  	check("UTF-8", "UTF-16");
  	check("UTF-8", "KOI8-R");
  	check("UTF-8", "ISO-8859-1");
  }
  
  TEST_CASE("translit() always returns the same value for the same inputs",
  	"[utils]")
  {
  	const std::vector<std::string> encodings {"UTF-8", "UTF-16", "KOI8-R", "ISO-8859-1"};
  	// (fromcode, tocode) -> result
  	std::map<std::pair<std::string, std::string>, std::string> results;
  
  	for (const auto& fromcode : encodings) {
  		for (const auto& tocode : encodings) {
  			results[std::make_pair(fromcode, tocode)] = utils::translit(tocode, fromcode);
  		}
  	}
  
  	for (const auto& fromcode : encodings) {
  		for (const auto& tocode : encodings) {
  			INFO(fromcode << " → " << tocode);
  			REQUIRE(utils::translit(tocode, fromcode) == results.at(std::make_pair(fromcode, tocode)));
  		}
  	}
  }
  