- Rename project name and package: novelWriter -> UmbraEdit, novelwriter -> umbraedit - Rename entry script novelWriter.py -> UmbraEdit.py - Rename package directory novelwriter/ -> umbraedit/ - Rename i18n translation sources nw_*.ts -> ue_*.ts - Rename packaging files (appdata, project xml template) - Update all imports, references, docs, workflows, tests and build config - Update pyproject.toml project name, GUI script and URLs - Preserve full upstream git history; origin repointed below
1050 lines
33 KiB
Python
1050 lines
33 KiB
Python
"""
|
|
UmbraEdit - Common Functions Tests
|
|
====================================
|
|
|
|
This file is a part of UmbraEdit.
|
|
|
|
Copyright (C) 2019 Veronica Berglyd Olsen and UmbraEdit contributors.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
""" # noqa
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
import weakref
|
|
|
|
from datetime import date
|
|
from pathlib import Path
|
|
from xml.etree import ElementTree as ET
|
|
|
|
import pytest
|
|
|
|
from PyQt6.QtCore import QMimeData, Qt, QUrl
|
|
from PyQt6.QtGui import QDesktopServices, QFont, QFontDatabase, QFontInfo
|
|
from PyQt6.QtWidgets import QWidget
|
|
from pytestqt.qtbot import QtBot
|
|
|
|
from umbraedit.common import (
|
|
appendIfSet,
|
|
checkBool,
|
|
checkDateNone,
|
|
checkFloat,
|
|
checkInt,
|
|
checkIntTuple,
|
|
checkPath,
|
|
checkString,
|
|
checkStringNone,
|
|
checkUuid,
|
|
compact,
|
|
decodeMimeHandles,
|
|
describeFont,
|
|
elide,
|
|
encodeMimeHandles,
|
|
firstFloat,
|
|
fontMatcher,
|
|
formatFileFilter,
|
|
formatInt,
|
|
formatLink,
|
|
formatPercent,
|
|
formatTime,
|
|
formatTimeStamp,
|
|
formatVersion,
|
|
fuzzyTime,
|
|
getFileSize,
|
|
hexToInt,
|
|
isHandle,
|
|
isItemClass,
|
|
isItemLayout,
|
|
isItemType,
|
|
isListInstance,
|
|
isTitleTag,
|
|
joinLines,
|
|
jsonCombine,
|
|
jsonEncode,
|
|
languageName,
|
|
makeFileNameSafe,
|
|
minmax,
|
|
numberToRoman,
|
|
openExternalPath,
|
|
processDialogSymbols,
|
|
processLangCode,
|
|
qtVisualAlign,
|
|
qtWeakLambda,
|
|
readTextFile,
|
|
safeExists,
|
|
safeIsDir,
|
|
safeIsFile,
|
|
safeIterDir,
|
|
simplified,
|
|
textDirection,
|
|
transferCase,
|
|
uniqueCompact,
|
|
utf16CharMap,
|
|
xmlElement,
|
|
xmlIndent,
|
|
xmlSubElem,
|
|
yesNo,
|
|
)
|
|
from umbraedit.types import QtAlignAbsolute, QtAlignCenter, QtAlignLeft, QtAlignRight
|
|
|
|
from tests.helpers import TstPaths, writeFile
|
|
from tests.mocked import causeOSError
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkStringNone():
|
|
"""Test the checkStringNone function."""
|
|
assert checkStringNone("Stuff", "NotNone") == "Stuff"
|
|
assert checkStringNone("None", "NotNone") is None
|
|
assert checkStringNone(None, "NotNone") is None
|
|
assert checkStringNone(1, "NotNone") == "NotNone"
|
|
assert checkStringNone(1.0, "NotNone") == "NotNone"
|
|
assert checkStringNone(True, "NotNone") == "NotNone"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkString():
|
|
"""Test the checkString function. Anything that is a string should
|
|
be returned, otherwise it returns the default.
|
|
"""
|
|
assert checkString("None", "default") == "None"
|
|
assert checkString("Text", "default") == "Text"
|
|
assert checkString(None, "default") == "default"
|
|
assert checkString(1, "default") == "default"
|
|
assert checkString(1.0, "default") == "default"
|
|
assert checkString(True, "default") == "default"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkInt():
|
|
"""Test the checkInt function. Anything that can be converted to an
|
|
integer should be returned, otherwise it returns the default.
|
|
"""
|
|
assert checkInt(1, 3) == 1
|
|
assert checkInt(1.0, 3) == 1
|
|
assert checkInt(True, 3) == 1
|
|
assert checkInt(False, 3) == 0
|
|
assert checkInt(None, 3) == 3
|
|
assert checkInt("1", 3) == 1
|
|
assert checkInt("1.0", 3) == 3
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkFloat():
|
|
"""Test the checkFloat function. Anything that can be converted to an
|
|
integer should be returned, otherwise it returns the default.
|
|
"""
|
|
assert checkFloat(1, 3.0) == 1.0
|
|
assert checkFloat(1.0, 3.0) == 1.0
|
|
assert checkFloat(True, 3.0) == 1.0
|
|
assert checkFloat(False, 3.0) == 0.0
|
|
assert checkFloat(None, 3.0) == 3.0
|
|
assert checkFloat("1", 3.0) == 1.0
|
|
assert checkFloat("1.0", 3.0) == 1.0
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkBool():
|
|
"""Test the checkBool function. Any bool, string version of Python
|
|
bool, or integer 1 or 0, are returned as bool. Otherwise, the
|
|
default is returned.
|
|
"""
|
|
# Bools
|
|
assert checkBool(True, False) is True
|
|
assert checkBool(False, True) is False
|
|
|
|
# Valid Strings
|
|
assert checkBool("True", False) is True
|
|
assert checkBool("False", True) is False
|
|
assert checkBool("true", False) is True
|
|
assert checkBool("false", True) is False
|
|
assert checkBool("Yes", False) is True
|
|
assert checkBool("No", True) is False
|
|
assert checkBool("yes", False) is True
|
|
assert checkBool("no", True) is False
|
|
assert checkBool("On", False) is True
|
|
assert checkBool("Off", True) is False
|
|
assert checkBool("on", False) is True
|
|
assert checkBool("off", True) is False
|
|
|
|
# Invalid Strings
|
|
assert checkBool("Foo", False) is False
|
|
assert checkBool("Foo", True) is True
|
|
assert checkBool("bar", False) is False
|
|
assert checkBool("bar", True) is True
|
|
|
|
# Valid Integers
|
|
assert checkBool(0, True) is False
|
|
assert checkBool(1, False) is True
|
|
|
|
# Invalid Integers
|
|
assert checkBool(2, True) is True
|
|
assert checkBool(2, False) is False
|
|
|
|
# Other Types
|
|
assert checkBool(None, True) is True
|
|
assert checkBool(None, False) is False
|
|
assert checkBool(0.0, True) is True
|
|
assert checkBool(1.0, False) is False
|
|
assert checkBool(2.0, True) is True
|
|
assert checkBool(2.0, False) is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkUuid():
|
|
"""Test the checkUuid function."""
|
|
testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea"
|
|
assert checkUuid("", None) is None # type: ignore
|
|
assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None # type: ignore
|
|
assert checkUuid("e2be99af-f9bf-qq03-857a-c3d1ac25abea", None) is None # type: ignore
|
|
assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abeaa", None) is None # type: ignore
|
|
assert checkUuid(testUuid, None) == testUuid # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkPath():
|
|
"""Test the checkPath function."""
|
|
assert checkPath(Path("test"), None) == Path("test") # type: ignore
|
|
assert checkPath("test", None) == Path("test") # type: ignore
|
|
assert checkPath(None, None) is None # type: ignore
|
|
assert checkPath("", None) is None # type: ignore
|
|
assert checkPath(" ", None) is None # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkDateNone():
|
|
"""Test the checkDateNone function."""
|
|
fallback = date(2020, 1, 1)
|
|
assert checkDateNone(date(2026, 7, 20), fallback) == date(2026, 7, 20)
|
|
assert checkDateNone("2026-07-20", fallback) == date(2026, 7, 20)
|
|
assert checkDateNone("not-a-date", fallback) == fallback
|
|
assert checkDateNone("", fallback) == fallback
|
|
assert checkDateNone(" ", fallback) == fallback
|
|
assert checkDateNone(None, fallback) == fallback
|
|
assert checkDateNone(1234, fallback) == fallback # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_isHandle():
|
|
"""Test the isHandle function."""
|
|
assert isHandle("47666c91c7ccf") is True
|
|
assert isHandle("47666C91C7CCF") is False
|
|
assert isHandle("h7666c91c7ccf") is False
|
|
assert isHandle("None") is False
|
|
assert isHandle(None) is False
|
|
assert isHandle("STUFF") is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_isTitleTag():
|
|
"""Test the isTitleTag function."""
|
|
assert isTitleTag("T1234") is True
|
|
|
|
assert isTitleTag("t1234") is False
|
|
assert isTitleTag("S1234") is False
|
|
assert isTitleTag("T123A") is False
|
|
assert isTitleTag("T12345") is False
|
|
|
|
assert isTitleTag("None") is False
|
|
assert isTitleTag(None) is False
|
|
assert isTitleTag("STUFF") is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_isItemClass():
|
|
"""Test the isItemClass function."""
|
|
assert isItemClass("NO_CLASS") is True
|
|
assert isItemClass("NOVEL") is True
|
|
assert isItemClass("PLOT") is True
|
|
assert isItemClass("CHARACTER") is True
|
|
assert isItemClass("WORLD") is True
|
|
assert isItemClass("TIMELINE") is True
|
|
assert isItemClass("OBJECT") is True
|
|
assert isItemClass("ENTITY") is True
|
|
assert isItemClass("CUSTOM") is True
|
|
assert isItemClass("ARCHIVE") is True
|
|
assert isItemClass("TRASH") is True
|
|
|
|
# Invalid
|
|
assert isItemClass("None") is False
|
|
assert isItemClass(None) is False
|
|
assert isItemClass("STUFF") is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_isItemType():
|
|
"""Test the isItemType function."""
|
|
assert isItemType("NO_TYPE") is True
|
|
assert isItemType("ROOT") is True
|
|
assert isItemType("FOLDER") is True
|
|
assert isItemType("FILE") is True
|
|
|
|
# Deprecated Type
|
|
assert isItemType("TRASH") is False
|
|
|
|
# Invalid
|
|
assert isItemType("None") is False
|
|
assert isItemType(None) is False # type: ignore
|
|
assert isItemType("STUFF") is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_isItemLayout():
|
|
"""Test the isItemLayout function."""
|
|
assert isItemLayout("NO_LAYOUT") is True
|
|
assert isItemLayout("DOCUMENT") is True
|
|
assert isItemLayout("NOTE") is True
|
|
|
|
# Deprecated Layouts
|
|
assert isItemLayout("TITLE") is False
|
|
assert isItemLayout("PAGE") is False
|
|
assert isItemLayout("BOOK") is False
|
|
assert isItemLayout("PARTITION") is False
|
|
assert isItemLayout("UNNUMBERED") is False
|
|
assert isItemLayout("CHAPTER") is False
|
|
assert isItemLayout("SCENE") is False
|
|
|
|
# Invalid
|
|
assert isItemLayout("None") is False
|
|
assert isItemLayout(None) is False # type: ignore
|
|
assert isItemLayout("STUFF") is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_isListInstance():
|
|
"""Test the isListInstance function."""
|
|
# String
|
|
assert isListInstance("stuff", str) is False
|
|
assert isListInstance(["stuff"], str) is True
|
|
|
|
# Int
|
|
assert isListInstance(1, int) is False
|
|
assert isListInstance([1], int) is True
|
|
|
|
# Mixed
|
|
assert isListInstance([1], str) is False
|
|
assert isListInstance(["stuff"], int) is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_hexToInt():
|
|
"""Test the hexToInt function."""
|
|
assert hexToInt(1) == 0
|
|
assert hexToInt("1") == 1
|
|
assert hexToInt("0xff") == 255
|
|
assert hexToInt("0xffff") == 65535
|
|
assert hexToInt("0xffffq") == 0
|
|
assert hexToInt("0xffffq", 12) == 12
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_minmax():
|
|
"""Test the minmax function."""
|
|
for i in range(-5, 15):
|
|
assert 0 <= minmax(i, 0, 10) <= 10
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_checkIntTuple():
|
|
"""Test the checkIntTuple function."""
|
|
assert checkIntTuple(0, (0, 1, 2), 3) == 0
|
|
assert checkIntTuple(5, (0, 1, 2), 3) == 3
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_firstFloat():
|
|
"""Test the firstFloat function."""
|
|
assert firstFloat(None, 1.0) == 1.0
|
|
assert firstFloat(1.0, None) == 1.0
|
|
assert firstFloat(None, 1) == 0.0
|
|
assert firstFloat(None, "1.0") == 0.0
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_formatTimeStamp():
|
|
"""Test the formatTimeStamp function."""
|
|
tTime = time.mktime(time.gmtime(0))
|
|
assert formatTimeStamp(tTime, False, "%Y") == "1970"
|
|
assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00"
|
|
assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00"
|
|
assert formatTimeStamp(tTime, False, 123) == "ERROR" # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_formatTime():
|
|
"""Test the formatTime function."""
|
|
assert formatTime("1") == "ERROR" # type: ignore
|
|
assert formatTime(1.0) == "ERROR" # type: ignore
|
|
assert formatTime(1) == "00:00:01"
|
|
assert formatTime(59) == "00:00:59"
|
|
assert formatTime(60) == "00:01:00"
|
|
assert formatTime(180) == "00:03:00"
|
|
assert formatTime(194) == "00:03:14"
|
|
assert formatTime(3540) == "00:59:00"
|
|
assert formatTime(3599) == "00:59:59"
|
|
assert formatTime(3600) == "01:00:00"
|
|
assert formatTime(11640) == "03:14:00"
|
|
assert formatTime(11655) == "03:14:15"
|
|
assert formatTime(86399) == "23:59:59"
|
|
assert formatTime(86400) == "1-00:00:00"
|
|
assert formatTime(360000) == "4-04:00:00"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_formatVersion():
|
|
"""Test the formatVersion function."""
|
|
assert formatVersion("1.2") == "1.2"
|
|
assert formatVersion("1.2.1") == "1.2 Patch 1"
|
|
assert formatVersion("1.2a1") == "1.2 Alpha 1"
|
|
assert formatVersion("1.2b2") == "1.2 Beta 2"
|
|
assert formatVersion("1.2rc3") == "1.2 RC 3"
|
|
assert formatVersion("26.1") == "2026.1"
|
|
assert formatVersion("26.1.1") == "2026.1 Patch 1"
|
|
assert formatVersion("26.1a1") == "2026.1 Alpha 1"
|
|
assert formatVersion("26.1b2") == "2026.1 Beta 2"
|
|
assert formatVersion("26.1rc3") == "2026.1 RC 3"
|
|
assert formatVersion("12345") == ""
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_formatFileFilter():
|
|
"""Test the formatFileFilter function."""
|
|
assert formatFileFilter(["*.txt"]) == "Text files (*.txt)"
|
|
assert formatFileFilter(["*.txt", "*"]) == "Text files (*.txt);;All files (*)"
|
|
assert formatFileFilter([("Stuff", "*.stuff"), "*.txt", "*"]) == (
|
|
"Stuff (*.stuff);;Text files (*.txt);;All files (*)"
|
|
)
|
|
|
|
# Invalid entries are skipped
|
|
assert formatFileFilter([123, ("Too", "Many", "Items"), "*.txt"]) == "Text files (*.txt)" # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_formatLink():
|
|
"""Test the formatLink function."""
|
|
assert formatLink("a") == "<a href='a'>a</a>"
|
|
assert formatLink("a", "b") == "<a href='a'>b</a>"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_simplified():
|
|
"""Test the simplified function."""
|
|
assert simplified("Hello World") == "Hello World"
|
|
assert simplified(" Hello World ") == "Hello World"
|
|
assert simplified("\tHello\n\r\tWorld") == "Hello World"
|
|
|
|
# Whitespace-like control characters are still treated as separators
|
|
assert simplified("Hello\x1cWorld") == "Hello World"
|
|
|
|
# Other control characters are dropped entirely, not just left as-is
|
|
assert simplified("Hello\x00\x07\x1bWorld") == "HelloWorld"
|
|
assert simplified("Hello \x7f World") == "Hello World"
|
|
|
|
# Lone surrogates are dropped too, as they can't be written to UTF-8 files
|
|
lone = b"abc\xff".decode("utf-8", "surrogateescape")
|
|
assert simplified(f"Hello {lone} World") == "Hello abc World"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_compact():
|
|
"""Test the compact function."""
|
|
assert compact("! ! !") == "!!!"
|
|
assert compact("1\t2\t3") == "123"
|
|
assert compact("1\n2\n3") == "123"
|
|
assert compact("1\r2\r3") == "123"
|
|
assert compact("1\u00a02\u00a03") == "123"
|
|
|
|
# Non-whitespace control characters and lone surrogates are dropped too
|
|
assert compact("1\x00\x07\x1b2") == "12"
|
|
lone = b"1\xff2".decode("utf-8", "surrogateescape")
|
|
assert compact(lone) == "12"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_uniqueCompact():
|
|
"""Test the uniqueCompact function."""
|
|
assert uniqueCompact("! ! !") == "!"
|
|
assert uniqueCompact("1\t2\t3") == "123"
|
|
assert uniqueCompact("1\n2\n3") == "123"
|
|
assert uniqueCompact("1\r2\r3") == "123"
|
|
assert uniqueCompact("1\u00a02\u00a03") == "123"
|
|
assert uniqueCompact("3 2 1") == "123"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_joinLines():
|
|
"""Test the joinLines function."""
|
|
assert joinLines("abc") == "abc"
|
|
assert joinLines(["abc"]) == "abc"
|
|
assert joinLines(["a", "b", "c"]) == "abc"
|
|
assert joinLines("abc", "\n") == "abc"
|
|
assert joinLines(["abc"], "\n") == "abc"
|
|
assert joinLines(["a", "b", "c"], "\n") == "a\nb\nc"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_appendIfSet():
|
|
"""Test the appendIfSet function."""
|
|
data = []
|
|
appendIfSet(data, "")
|
|
assert data == []
|
|
appendIfSet(data, "a")
|
|
assert data == ["a"]
|
|
appendIfSet(data, "b")
|
|
assert data == ["a", "b"]
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_processDialogSymbols():
|
|
"""Test the processDialogSymbols function."""
|
|
assert processDialogSymbols("abc") == ""
|
|
assert processDialogSymbols("\u00ab\u00ab\u00bb\u00bb") == "\u00ab\u00bb"
|
|
assert processDialogSymbols("-\u2013\u2014\u2015") == "\u2013\u2014\u2015"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_processLangCode():
|
|
"""Test the processLangCode function."""
|
|
assert processLangCode("") == ""
|
|
assert processLangCode(" ") == ""
|
|
assert processLangCode("en") == "en-US"
|
|
assert processLangCode("en_gb") == "en-GB"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_languageName():
|
|
"""Test the languageName function."""
|
|
assert languageName("en-GB") == "British English"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_elide():
|
|
"""Test the elide function."""
|
|
assert elide("Hello World!", 12) == "Hello World!"
|
|
assert elide("Hello World!", 11) == "Hello W ..."
|
|
assert elide("Hello World!", 10) == "Hello ..."
|
|
assert elide("Hello World!", 9) == "Hello ..."
|
|
assert elide("Hello World!", 8) == "Hell ..."
|
|
assert elide("Hello World!", 7) == "Hel ..."
|
|
assert elide("Hello World!", 6) == "He ..."
|
|
assert elide("Hello World!", 5) == "H ..."
|
|
assert elide("Hello World!", 4) == " ..."
|
|
assert elide("Hello World!", 3) == " ..."
|
|
assert elide("Hello World!", 2) == " ..."
|
|
assert elide("Hello World!", 1) == " ..."
|
|
assert elide("Hello World!", 0) == " ..."
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_textDirection():
|
|
"""Test the textDirection function."""
|
|
assert textDirection("") == ""
|
|
assert textDirection("1234 - !?") == ""
|
|
assert textDirection("Text") == "ltr"
|
|
assert textDirection("12 Text") == "ltr"
|
|
assert textDirection("متن") == "rtl"
|
|
assert textDirection("שלום") == "rtl"
|
|
assert textDirection("12 متن Text") == "rtl"
|
|
assert textDirection("[i]متن") == "ltr"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_yesNo():
|
|
"""Test the yesNo function."""
|
|
# Bool
|
|
assert yesNo(True) == "yes"
|
|
assert yesNo(False) == "no"
|
|
|
|
# None
|
|
assert yesNo(None) == "no"
|
|
|
|
# String
|
|
assert yesNo("foo") == "yes" # type: ignore
|
|
assert yesNo("") == "no" # type: ignore
|
|
|
|
# Integer
|
|
assert yesNo(0) == "no"
|
|
assert yesNo(1) == "yes"
|
|
assert yesNo(2) == "yes"
|
|
|
|
# Float
|
|
assert yesNo(0.0) == "no" # type: ignore
|
|
assert yesNo(1.0) == "yes" # type: ignore
|
|
assert yesNo(2.0) == "yes" # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_formatInt():
|
|
"""Test the formatInt function."""
|
|
# Normal Cases
|
|
assert formatInt(1) == "1"
|
|
assert formatInt(12) == "12"
|
|
assert formatInt(123) == "123"
|
|
assert formatInt(1234) == "1.23\u2009k"
|
|
assert formatInt(12345) == "12.3\u2009k"
|
|
assert formatInt(123456) == "123\u2009k"
|
|
assert formatInt(1234567) == "1.23\u2009M"
|
|
assert formatInt(12345678) == "12.3\u2009M"
|
|
assert formatInt(123456789) == "123\u2009M"
|
|
assert formatInt(1234567890) == "1.23\u2009G"
|
|
|
|
# Beyond the largest supported suffix, the raw value is returned
|
|
assert formatInt(10**22) == str(10**22)
|
|
|
|
# Exceptions
|
|
assert formatInt(12.3) == "ERR" # type: ignore
|
|
assert formatInt(None) == "ERR" # type: ignore
|
|
assert formatInt("42") == "ERR" # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_formatPercent():
|
|
"""Test the formatPercent function."""
|
|
# Plain fraction, no divisor
|
|
assert formatPercent(0.5) == "50.0\u202f%"
|
|
assert formatPercent(1.0) == "100.0\u202f%"
|
|
assert formatPercent(0) == "0.0\u202f%"
|
|
|
|
# With a divisor
|
|
assert formatPercent(50, divisor=100) == "50.0\u202f%"
|
|
assert formatPercent(1, divisor=3) == "33.3\u202f%"
|
|
|
|
# Divisor of zero is ignored, value is used as-is
|
|
assert formatPercent(0, divisor=0) == "0.0\u202f%"
|
|
|
|
# Precision
|
|
assert formatPercent(1, divisor=3, prec=0) == "33\u202f%"
|
|
assert formatPercent(1, divisor=3, prec=3) == "33.333\u202f%"
|
|
|
|
# Exceptions
|
|
assert formatPercent(None) == "ERR" # type: ignore
|
|
assert formatPercent("42") == "ERR" # type: ignore
|
|
assert formatPercent("42", divisor=3) == "ERR" # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_transferCase():
|
|
"""Test the transferCase function."""
|
|
assert transferCase(1, "TaRgEt") == "TaRgEt" # type: ignore
|
|
assert transferCase("source", 1) == 1 # type: ignore
|
|
assert transferCase("", "TaRgEt") == "TaRgEt"
|
|
assert transferCase("source", "") == ""
|
|
assert transferCase("Source", "target") == "Target"
|
|
assert transferCase("SOURCE", "target") == "TARGET"
|
|
assert transferCase("source", "TARGET") == "target"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_fuzzyTime():
|
|
"""Test the fuzzyTime function."""
|
|
assert fuzzyTime(-1) == "in the future"
|
|
assert fuzzyTime(0) == "just now"
|
|
assert fuzzyTime(29) == "just now"
|
|
assert fuzzyTime(30) == "a minute ago"
|
|
assert fuzzyTime(89) == "a minute ago"
|
|
assert fuzzyTime(90) == "2 minutes ago"
|
|
assert fuzzyTime(149) == "2 minutes ago"
|
|
assert fuzzyTime(151) == "3 minutes ago"
|
|
assert fuzzyTime(3299) == "55 minutes ago"
|
|
assert fuzzyTime(3300) == "an hour ago"
|
|
assert fuzzyTime(5399) == "an hour ago"
|
|
assert fuzzyTime(5400) == "2 hours ago"
|
|
assert fuzzyTime(84599) == "23 hours ago"
|
|
assert fuzzyTime(84600) == "a day ago"
|
|
assert fuzzyTime(129599) == "a day ago"
|
|
assert fuzzyTime(129600) == "2 days ago"
|
|
assert fuzzyTime(561599) == "6 days ago"
|
|
assert fuzzyTime(561600) == "a week ago"
|
|
assert fuzzyTime(907199) == "a week ago"
|
|
assert fuzzyTime(907200) == "2 weeks ago"
|
|
assert fuzzyTime(2419199) == "4 weeks ago"
|
|
assert fuzzyTime(2419200) == "a month ago"
|
|
assert fuzzyTime(3887999) == "a month ago"
|
|
assert fuzzyTime(3888000) == "2 months ago"
|
|
assert fuzzyTime(29807999) == "11 months ago"
|
|
assert fuzzyTime(29808000) == "a year ago"
|
|
assert fuzzyTime(47336399) == "a year ago"
|
|
assert fuzzyTime(47336400) == "2 years ago"
|
|
|
|
|
|
@pytest.mark.core
|
|
def testCommon_numberToRoman():
|
|
"""Test conversion of integers to Roman numbers."""
|
|
assert numberToRoman(None, False) == "NAN" # type: ignore
|
|
assert numberToRoman(0, False) == "OOR"
|
|
assert numberToRoman(1, False) == "I"
|
|
assert numberToRoman(2, False) == "II"
|
|
assert numberToRoman(3, False) == "III"
|
|
assert numberToRoman(4, False) == "IV"
|
|
assert numberToRoman(5, False) == "V"
|
|
assert numberToRoman(6, False) == "VI"
|
|
assert numberToRoman(7, False) == "VII"
|
|
assert numberToRoman(8, False) == "VIII"
|
|
assert numberToRoman(9, False) == "IX"
|
|
assert numberToRoman(10, False) == "X"
|
|
assert numberToRoman(14, False) == "XIV"
|
|
assert numberToRoman(42, False) == "XLII"
|
|
assert numberToRoman(99, False) == "XCIX"
|
|
assert numberToRoman(142, False) == "CXLII"
|
|
assert numberToRoman(542, False) == "DXLII"
|
|
assert numberToRoman(999, False) == "CMXCIX"
|
|
assert numberToRoman(2010, False) == "MMX"
|
|
assert numberToRoman(999, True) == "cmxcix"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_safeIterDir(fncPath: Path):
|
|
"""Test the safeIterDir function."""
|
|
files = [
|
|
fncPath / "file1.txt",
|
|
fncPath / "file2.txt",
|
|
fncPath / "file3.txt",
|
|
]
|
|
for file in files:
|
|
file.touch()
|
|
|
|
assert sorted(safeIterDir(fncPath, alert=True)) == files
|
|
assert list(safeIterDir(None, alert=True)) == [] # type: ignore
|
|
assert list(safeIterDir(None)) == [] # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_safeExists(fncPath: Path):
|
|
"""Test the safeExists function."""
|
|
file = fncPath / "file1.txt"
|
|
file.touch()
|
|
|
|
assert safeExists(file, alert=True) is True
|
|
assert safeExists(None, alert=True) is False # type: ignore
|
|
assert safeExists(None) is False # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_safeIsFile(fncPath: Path):
|
|
"""Test the safeIsFile function."""
|
|
file = fncPath / "file1.txt"
|
|
file.touch()
|
|
|
|
assert safeIsFile(file, alert=True) is True
|
|
assert safeIsFile(None, alert=True) is False # type: ignore
|
|
assert safeIsFile(None) is False # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_safeIsDir(fncPath: Path):
|
|
"""Test the safeIsDir function."""
|
|
folder = fncPath / "folder1"
|
|
folder.mkdir()
|
|
|
|
assert safeIsDir(folder, alert=True) is True
|
|
assert safeIsDir(None, alert=True) is False # type: ignore
|
|
assert safeIsDir(None) is False # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_describeFont():
|
|
"""Test the describeFont function."""
|
|
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont)
|
|
font.setPointSize(12)
|
|
assert describeFont(font).startswith("12 pt")
|
|
assert describeFont(None) == "Error" # type: ignore
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_fontMatcher(monkeypatch: pytest.MonkeyPatch):
|
|
"""Test the fontMatcher function."""
|
|
# Nonsense font is just returned
|
|
nonsense = QFont("nonesense", 10)
|
|
assert fontMatcher(nonsense) is nonsense
|
|
|
|
# Style is reset
|
|
nonsense.setStyleName("blabla")
|
|
assert nonsense.styleName() == "blabla"
|
|
assert fontMatcher(nonsense).styleName() == ""
|
|
|
|
# General font
|
|
if len(QFontDatabase.families()) > 1:
|
|
fontOne = QFont(QFontDatabase.families()[0])
|
|
fontTwo = QFont(QFontDatabase.families()[1])
|
|
check = QFont(fontOne)
|
|
check.setFamily(fontTwo.family())
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(QFontInfo, "family", lambda *a: "nonesense")
|
|
assert fontMatcher(check).family() == fontTwo.family()
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_encodeDecodeMimeHandles(monkeypatch: pytest.MonkeyPatch):
|
|
"""Test the encodeMimeHandles and decodeMimeHandles functions."""
|
|
handles = ["0123456789abc", "123456789abcd", "23456789abcde"]
|
|
mimeData = QMimeData()
|
|
encodeMimeHandles(mimeData, handles)
|
|
assert decodeMimeHandles(mimeData) == handles
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_utf16CharMap(monkeypatch: pytest.MonkeyPatch):
|
|
"""Test the utf16CharMap function."""
|
|
assert utf16CharMap("abc") == [0, 1, 2, 3]
|
|
assert utf16CharMap("a\u2014b\u2014c") == [0, 1, 2, 3, 4, 5]
|
|
assert utf16CharMap("a\U0001f605b\U0001f605c") == [0, 1, 3, 4, 6, 7]
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_jsonEncode():
|
|
"""Test the jsonEncode function."""
|
|
# Wrong type
|
|
assert jsonEncode(None) == "[]" # type: ignore
|
|
|
|
# Correct types
|
|
assert jsonEncode([1, 2]) == "[\n 1,\n 2\n]"
|
|
assert jsonEncode((1, 2)) == "[\n 1,\n 2\n]"
|
|
assert jsonEncode({1: 2}) == '{\n "1": 2\n}'
|
|
|
|
tstDict = {
|
|
"null": None,
|
|
"one": 1,
|
|
"two": "2",
|
|
"three": 3.0,
|
|
"four": False,
|
|
"five": (1, 2),
|
|
"six": {"a": 1, "b": 2},
|
|
"seven": [],
|
|
"eight": {},
|
|
}
|
|
|
|
# Complex Structure
|
|
assert jsonEncode(tstDict) == (
|
|
"{\n"
|
|
' "null": null,\n'
|
|
' "one": 1,\n'
|
|
' "two": "2",\n'
|
|
' "three": 3.0,\n'
|
|
' "four": false,\n'
|
|
' "five": [\n'
|
|
" 1,\n"
|
|
" 2\n"
|
|
" ],\n"
|
|
' "six": {\n'
|
|
' "a": 1,\n'
|
|
' "b": 2\n'
|
|
" },\n"
|
|
' "seven": [],\n'
|
|
' "eight": {}\n'
|
|
"}"
|
|
)
|
|
|
|
# Additional Indent
|
|
assert jsonEncode(tstDict, n=2) == (
|
|
"{\n"
|
|
' "null": null,\n'
|
|
' "one": 1,\n'
|
|
' "two": "2",\n'
|
|
' "three": 3.0,\n'
|
|
' "four": false,\n'
|
|
' "five": [\n'
|
|
" 1,\n"
|
|
" 2\n"
|
|
" ],\n"
|
|
' "six": {\n'
|
|
' "a": 1,\n'
|
|
' "b": 2\n'
|
|
" },\n"
|
|
' "seven": [],\n'
|
|
' "eight": {}\n'
|
|
" }"
|
|
)
|
|
|
|
# Max Indent
|
|
assert jsonEncode(tstDict, n=0, nmax=1) == (
|
|
"{\n"
|
|
' "null": null,\n'
|
|
' "one": 1,\n'
|
|
' "two": "2",\n'
|
|
' "three": 3.0,\n'
|
|
' "four": false,\n'
|
|
' "five": [1, 2],\n'
|
|
' "six": {"a": 1, "b": 2},\n'
|
|
' "seven": [],\n'
|
|
' "eight": {}\n'
|
|
"}"
|
|
)
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_jsonCombine():
|
|
"""Test the jsonCombine function."""
|
|
assert jsonCombine({"a": "[1, 2]", "b": "[3, 4]"}) == ('{\n "a": [1, 2],\n "b": [3, 4]\n}\n')
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_xmlIndent():
|
|
"""Test the xmlIndent function."""
|
|
xRoot = ET.fromstring("<xml><group><item>foo</item></group></xml>")
|
|
xmlIndent(ET.ElementTree(xRoot))
|
|
assert ET.tostring(xRoot) == (b"<xml>\n <group>\n <item>foo</item>\n </group>\n</xml>\n")
|
|
|
|
# If we send nonsense, nothing is done
|
|
data = "foobar"
|
|
xmlIndent(data) # type: ignore
|
|
assert data == "foobar"
|
|
|
|
# An element with no children has no child indentation applied
|
|
xEmpty = ET.fromstring("<xml />")
|
|
xmlIndent(xEmpty)
|
|
assert ET.tostring(xEmpty) == b"<xml />\n"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_xmlElement():
|
|
"""Test the xmlElement function."""
|
|
assert ET.tostring(xmlElement("node", None, attrib={"a": "b"})) == b'<node a="b" />'
|
|
assert ET.tostring(xmlElement("node", "text", attrib={"a": "b"})) == b'<node a="b">text</node>'
|
|
assert ET.tostring(xmlElement("node", "text", tail="foo", attrib={"a": "b"})) == b'<node a="b">text</node>foo'
|
|
assert ET.tostring(xmlElement("node", 42, attrib={"a": "b"})) == b'<node a="b">42</node>'
|
|
assert ET.tostring(xmlElement("node", 3.14, attrib={"a": "b"})) == b'<node a="b">3.14</node>'
|
|
assert ET.tostring(xmlElement("node", True, attrib={"a": "b"})) == b'<node a="b">true</node>'
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_xmlSubElem():
|
|
"""Test the xmlSubElem function."""
|
|
assert ET.tostring(xmlSubElem(ET.Element("r"), "node", None, attrib={"a": "b"})) == b'<node a="b" />'
|
|
assert ET.tostring(xmlSubElem(ET.Element("r"), "node", "text", attrib={"a": "b"})) == b'<node a="b">text</node>'
|
|
assert (
|
|
ET.tostring(xmlSubElem(ET.Element("r"), "node", "text", tail="foo", attrib={"a": "b"}))
|
|
== b'<node a="b">text</node>foo'
|
|
)
|
|
assert ET.tostring(xmlSubElem(ET.Element("r"), "node", 42, attrib={"a": "b"})) == b'<node a="b">42</node>'
|
|
assert ET.tostring(xmlSubElem(ET.Element("r"), "node", 3.14, attrib={"a": "b"})) == b'<node a="b">3.14</node>'
|
|
assert ET.tostring(xmlSubElem(ET.Element("r"), "node", True, attrib={"a": "b"})) == b'<node a="b">true</node>'
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_readTextFile(monkeypatch: pytest.MonkeyPatch, fncPath: Path, ipsumText: list[str]):
|
|
"""Test the readTextFile function."""
|
|
testText = "\n\n".join(ipsumText) + "\n"
|
|
testFile = fncPath / "ipsum.txt"
|
|
writeFile(testFile, testText)
|
|
|
|
assert readTextFile(fncPath / "not_a_file.txt") == ""
|
|
assert readTextFile(testFile) == testText
|
|
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("pathlib.Path.read_text", causeOSError)
|
|
assert readTextFile(testFile) == ""
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_makeFileNameSafe():
|
|
"""Test the makeFileNameSafe function."""
|
|
# Trim edges
|
|
assert makeFileNameSafe(" Name ") == "Name"
|
|
|
|
# Normalise Unicode
|
|
assert makeFileNameSafe("Stuff œfi2⁵") == "Stuff œfi25"
|
|
|
|
# No control characters
|
|
assert makeFileNameSafe("One\tTwo") == "OneTwo"
|
|
assert makeFileNameSafe("One\nTwo") == "OneTwo"
|
|
assert makeFileNameSafe("One\rTwo") == "OneTwo"
|
|
|
|
# Invalid special characters
|
|
assert makeFileNameSafe("One\\Two") == "OneTwo"
|
|
assert makeFileNameSafe("One/Two") == "OneTwo"
|
|
assert makeFileNameSafe("One:Two") == "OneTwo"
|
|
assert makeFileNameSafe("One*Two") == "OneTwo"
|
|
assert makeFileNameSafe("One?Two") == "OneTwo"
|
|
assert makeFileNameSafe('One"Two') == "OneTwo"
|
|
assert makeFileNameSafe("One<Two") == "OneTwo"
|
|
assert makeFileNameSafe("One>Two") == "OneTwo"
|
|
assert makeFileNameSafe("One|Two") == "OneTwo"
|
|
|
|
# Names that are valid
|
|
assert makeFileNameSafe("One Two") == "One Two"
|
|
assert makeFileNameSafe("One,Two") == "One,Two"
|
|
assert makeFileNameSafe("One-Two") == "One-Two"
|
|
assert makeFileNameSafe("One\u2013Two") == "One\u2013Two"
|
|
assert makeFileNameSafe("One\u2014Two") == "One\u2014Two"
|
|
assert makeFileNameSafe("Bob's Story") == "Bob's Story"
|
|
|
|
# Unicode
|
|
assert makeFileNameSafe("æøå") == "æøå"
|
|
assert makeFileNameSafe("ßÜ") == "ßÜ"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_getFileSize(fncPath: Path):
|
|
"""Test the getFileSize function."""
|
|
(fncPath / "one.txt").write_bytes(b"foobar")
|
|
(fncPath / "two.txt").touch()
|
|
|
|
assert getFileSize(fncPath / "nope.txt") == -1
|
|
assert getFileSize(fncPath / "one.txt") == 6
|
|
assert getFileSize(fncPath / "two.txt") == 0
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_openExternalPath(monkeypatch: pytest.MonkeyPatch, tstPaths: TstPaths):
|
|
"""Test the openExternalPath function."""
|
|
lastUrl = ""
|
|
|
|
def mockOpenUrl(url: QUrl) -> None:
|
|
nonlocal lastUrl
|
|
lastUrl = url.toString()
|
|
|
|
monkeypatch.setattr(QDesktopServices, "openUrl", mockOpenUrl)
|
|
assert openExternalPath(Path("/foo/bar")) is False
|
|
assert openExternalPath(tstPaths.tmpDir) is True
|
|
assert lastUrl.startswith("file://")
|
|
|
|
|
|
@pytest.mark.base
|
|
def testCommon_qtWeakLambda():
|
|
"""Test the qtWeakLambda function."""
|
|
|
|
class Counter:
|
|
count = 0
|
|
|
|
def add(self, step: int) -> None:
|
|
self.count += step
|
|
|
|
obj = Counter()
|
|
wrapper = qtWeakLambda(obj.add, 2)
|
|
|
|
# The wrapper calls the method, but does not keep the object alive
|
|
wrapper()
|
|
assert obj.count == 2
|
|
|
|
# When the object is gone, the wrapper does nothing
|
|
ref = weakref.ref(obj)
|
|
del obj
|
|
assert ref() is None
|
|
wrapper()
|
|
|
|
|
|
@pytest.mark.gui
|
|
def testCommon_qtVisualAlign(qtbot: QtBot):
|
|
"""Test the qtVisualAlign function."""
|
|
widget = QWidget()
|
|
qtbot.addWidget(widget)
|
|
assert qtVisualAlign(widget, QtAlignLeft) == QtAlignAbsolute | QtAlignLeft
|
|
assert qtVisualAlign(widget, QtAlignRight) == QtAlignAbsolute | QtAlignRight
|
|
|
|
widget.setLayoutDirection(Qt.LayoutDirection.RightToLeft)
|
|
assert qtVisualAlign(widget, QtAlignLeft) == QtAlignAbsolute | QtAlignRight
|
|
assert qtVisualAlign(widget, QtAlignRight) == QtAlignAbsolute | QtAlignLeft
|
|
assert qtVisualAlign(widget, QtAlignCenter) == QtAlignCenter
|