Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mumble-voip/mumble.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src/tests
diff options
context:
space:
mode:
authorRobert Adam <dev@robert-adam.de>2021-11-03 20:25:09 +0300
committerRobert Adam <dev@robert-adam.de>2022-02-07 22:10:23 +0300
commit2480e1342e5ad20db3af4711efe7ead3193197ed (patch)
treeeb621aea4d6073c38d8b1cda38a1b6b0a0b1f351 /src/tests
parent900378756c57f0a0d6bc4cc2a26ca437e40cbc1a (diff)
TEST(client): Settings JSON serialization
Diffstat (limited to 'src/tests')
-rw-r--r--src/tests/CMakeLists.txt4
-rw-r--r--src/tests/TestSettingsJSONSerialization/CMakeLists.txt55
-rwxr-xr-xsrc/tests/TestSettingsJSONSerialization/generate_test_case.py306
3 files changed, 365 insertions, 0 deletions
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 438dd91e6..87392a1c3 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -16,6 +16,10 @@ endmacro()
if(client)
use_test("TestXMLTools")
+ if(NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "FreeBSD")
+ # For some reason Qt segfaults when executing this test on FreeBSD without a display (even when using the offscreen plugin)
+ use_test("TestSettingsJSONSerialization")
+ endif()
endif()
if(server)
diff --git a/src/tests/TestSettingsJSONSerialization/CMakeLists.txt b/src/tests/TestSettingsJSONSerialization/CMakeLists.txt
new file mode 100644
index 000000000..8515c6a40
--- /dev/null
+++ b/src/tests/TestSettingsJSONSerialization/CMakeLists.txt
@@ -0,0 +1,55 @@
+# Copyright 2021 The Mumble Developers. All rights reserved.
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file at the root of the
+# Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+find_pkg(Qt5 COMPONENTS Widgets REQUIRED)
+
+# kpCertificate: We can't create a value for that on-the-fly, so we have to exclude it from the test
+# All other: These settings are not saved
+set(IGNORED_FIELDS
+ "kpCertificate,bSuppressIdentity,lmLoopMode,dPacketLoss,dMaxPacketDelay,requireRestartToApply,settingsLocation,createdSettingsBackup")
+
+include(FindPythonInterpreter)
+
+set(PYTHON_HINTS
+ "C:/Python39-x64" # Path on the AppVeyor CI server
+)
+
+find_python_interpreter(
+ VERSION 3
+ INTERPRETER_OUT_VAR PYTHON_INTERPRETER
+ HINTS ${PYTHON_HINTS}
+ REQUIRED
+)
+
+set(TEST_CASE_SOURCE_FILE "${CMAKE_CURRENT_BINARY_DIR}/TestSettingsJSONSerialization.cpp")
+set(SETTINGS_HEADER "${CMAKE_SOURCE_DIR}/src/mumble/Settings.h")
+
+if(NOT EXISTS "${SETTINGS_HEADER}")
+ message(FATAL_ERROR "Unable to locate Settings header at \"${SETTINGS_HEADER}\"")
+endif()
+
+# Generate the test-case's source file
+add_custom_command(
+ OUTPUT "${TEST_CASE_SOURCE_FILE}"
+ COMMAND "${PYTHON_INTERPRETER}" "${CMAKE_CURRENT_SOURCE_DIR}/generate_test_case.py" --settings-header "${SETTINGS_HEADER}"
+ --settings-struct-nam Settings --ignore-fields "${IGNORED_FIELDS}" --output-file "${TEST_CASE_SOURCE_FILE}"
+ COMMENT "Generating test-case for 'SettingsJSONSerialization'"
+ DEPENDS "generate_test_case.py" "${CMAKE_SOURCE_DIR}/src/mumble/Settings.h"
+ "${CMAKE_SOURCE_DIR}/src/mumble/JSONSerialization.h"
+)
+
+add_executable(TestSettingsJSONSerialization "${TEST_CASE_SOURCE_FILE}")
+target_link_libraries(TestSettingsJSONSerialization PRIVATE mumble_client_object_lib)
+target_link_libraries(TestSettingsJSONSerialization PRIVATE Qt5::Test)
+
+set_target_properties(TestSettingsJSONSerialization PROPERTIES AUTOMOC ON)
+
+
+add_test(
+ NAME TestSettingsJSONSerialization
+ COMMAND $<TARGET_FILE:TestSettingsJSONSerialization>
+ # Specifying the working directory is necessary, to make sure the dependent DLLs are found (on Windows)
+ WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
+)
diff --git a/src/tests/TestSettingsJSONSerialization/generate_test_case.py b/src/tests/TestSettingsJSONSerialization/generate_test_case.py
new file mode 100755
index 000000000..e1c4e52a2
--- /dev/null
+++ b/src/tests/TestSettingsJSONSerialization/generate_test_case.py
@@ -0,0 +1,306 @@
+#!/usr/bin/env python3
+
+import argparse
+import re
+from collections import OrderedDict
+
+defaultValues = {}
+structs = {}
+
+def extractFields(classDefinition):
+ # Bring all declarations on a single line
+ classDefinition = re.sub(r",\s+", ",", classDefinition)
+ # Make sure there are no spaces in templates
+ classDefinition = classDefinition.replace("< ", "<").replace(" >", ">")
+ # Remove function definitions (aka: lines that contain a parenthesis)
+ classDefinition = re.sub(r".*\(.*", "", classDefinition)
+ # Remove enum declarations
+ classDefinition = re.sub(r"enum\s*\w+\s*\{.*?\};", "", classDefinition, flags=re.DOTALL)
+ # remove semuicolons
+ classDefinition = classDefinition.replace(";", "")
+ # Remove "unsigned" type specifier
+ classDefinition = classDefinition.replace("unsigned ", "")
+ # Remove "mutable" keyword
+ classDefinition = classDefinition.replace("mutable ", "")
+ # Remove comments
+ classDefinition = re.sub(r"//.*", "", classDefinition)
+
+ fields = OrderedDict()
+ for currentLine in classDefinition.split("\n"):
+ currentLine = currentLine.strip()
+
+ if currentLine.startswith("//") or currentLine in ["private:", "public:"]:
+ continue
+
+ words = currentLine.split()
+
+ if len(words) == 0:
+ continue
+
+ if words[0] in ["enum", "typedef", "struct"] or "static" in words:
+ continue
+
+ if len(words) > 2:
+ # Comma-separated lists
+ names = "".join(words[1:])
+ words = [words[0], names]
+
+ assert len(words) == 2, "Expected remaining lines to be of form <type> <name(s)>"
+
+ for currentFieldName in words[1].split(","):
+ currentFieldName = currentFieldName.strip()
+
+ if not currentFieldName:
+ continue
+
+ fields[currentFieldName] = words[0]
+
+ return fields
+
+
+def extractClassDefinition(className, contents, classIdentifier = "class"):
+ definition = contents[contents.find(classIdentifier + " " + className) :]
+ curlyBrackets = 0
+ endIndex = 0
+ for c in definition:
+ if c == '{':
+ curlyBrackets += 1
+ elif c == '}':
+ curlyBrackets -= 1
+
+ if curlyBrackets == 0:
+ break
+
+ endIndex += 1
+
+ assert curlyBrackets == 0
+
+ definition = definition[0 : endIndex]
+
+ return definition
+
+
+def getTemplateArguments(templateDef):
+ relevantContent = templateDef[templateDef.find("<") + 1 : templateDef.rfind(">")]
+
+ # Nested templates not yet supported
+ assert not "<" in relevantContent
+
+ args = relevantContent.split(",")
+ args = [x.strip() for x in args]
+
+ return args
+
+
+def getDefaultValueForType(dataType):
+ if dataType in ["int", "short", "long", "float", "double", "qreal"] or dataType.startswith("qint") or dataType.startswith("quint") or \
+ dataType.startswith("uint"):
+ return "42"
+ elif dataType in ["bool"]:
+ return "true"
+ elif dataType in ["QString", "std::string"]:
+ return "\"My String\""
+ elif dataType in ["QByteArray"]:
+ return "QByteArray::fromStdString(\"My ByteArray\")"
+ elif dataType in ["QPoint"]:
+ return "{ 4, 2 }"
+ elif dataType in ["QVariant"]:
+ return "QVariant(15)"
+ elif dataType in ["QStringList"]:
+ return "QStringList({ QStringLiteral(\"Another string\") })"
+ elif dataType in ["QColor"]:
+ return "QColor(QLatin1String(\"indigo\"))"
+ elif dataType in ["QFont"]:
+ return "QFont(QLatin1String(\"Helvetica\"))"
+ elif dataType in ["QRect", "QRectF"]:
+ return "{ 3, 5, 10, 7 }"
+ elif dataType in ["QSize", "QSizeF"]:
+ return "{ 8, 12 }"
+ elif dataType in ["AudioTransmit"]:
+ return "Settings::PushToTalk"
+ elif dataType in ["VADSource"]:
+ return "Settings::SignalToNoise"
+ elif dataType in ["LoopMode"]:
+ return "Settings::Server"
+ elif dataType in ["ChannelExpand"]:
+ return "Settings::AllChannels"
+ elif dataType in ["ChannelDrag"]:
+ return "Settings::DoNothing"
+ elif dataType in ["ServerShow"]:
+ return "Settings::ShowPopulated"
+ elif dataType in ["IdleAction"]:
+ return "Settings::Deafen"
+ elif dataType in ["NoiseCancel"]:
+ return "Settings::NoiseCancelSpeex"
+ elif dataType in ["EchoCancelOptionID"]:
+ return "EchoCancelOptionID::SPEEX_MULTICHANNEL"
+ elif dataType in ["OverlayShow"]:
+ return "OverlaySettings::HomeChannel"
+ elif dataType in ["OverlayShow"]:
+ return "OverlaySettings::HomeChannel"
+ elif dataType in ["OverlaySort"]:
+ return "OverlaySettings::LastStateChange"
+ elif dataType in ["OverlayExclusionMode"]:
+ return "OverlaySettings::BlacklistExclusionMode"
+ elif dataType in ["Qt::Alignment"]:
+ return "Qt::AlignJustify | Qt::AlignBaseline"
+ elif dataType in ["WindowLayout"]:
+ return "Settings::LayoutHybrid"
+ elif dataType in ["AlwaysOnTopBehaviour"]:
+ return "Settings::OnTopAlways"
+ elif dataType in ["Search::SearchDialog::UserAction"]:
+ return "Search::SearchDialog::UserAction::NONE"
+ elif dataType in ["Search::SearchDialog::ChannelAction"]:
+ return "Search::SearchDialog::ChannelAction::NONE"
+ elif dataType in ["ProxyType"]:
+ return "Settings::Socks5Proxy"
+ elif dataType in ["RecordingMode"]:
+ return "Settings::RecordingMultichannel"
+ elif dataType.startswith("QMap") or dataType.startswith("QHash"):
+ types = getTemplateArguments(dataType)
+
+ assert len(types) == 2
+
+ return "{ {" + getDefaultValueForType(types[0]) + ", " + getDefaultValueForType(types[1]) + "} }"
+ elif dataType.startswith("QList"):
+ types = getTemplateArguments(dataType)
+
+ assert len(types) == 1
+
+ return "{ " + getDefaultValueForType(types[0]) + " }"
+ elif dataType.startswith("std::array"):
+ args = getTemplateArguments(dataType)
+
+ # std::array< Type, Size >
+ assert len(args) == 2
+
+ args[1] = int(args[1])
+
+ string = "{ "
+ for _ in range(args[1]):
+ string += getDefaultValueForType(args[0]) + ", "
+
+ if args[1] > 0:
+ # remove trailing comma
+ string = string[:-len(", ")]
+
+ return string + " }"
+ elif dataType in ["KeyPair"]:
+ # We can't really create a certificate here, so we have to use a default-constructed value
+ return "{}"
+
+ if dataType in defaultValues:
+ return defaultValues[dataType]
+
+ raise RuntimeError("No known default value for type " + dataType)
+
+
+
+def generateTestBody(settingsFields, settingsClassName, excludeFields = []):
+ contents = "#include <QObject>\n"
+ contents += "#include <QtTest>\n"
+ contents += "#include \"" + settingsClassName + ".h\"\n"
+ contents += "#include \"JSONSerialization.h\"\n"
+ contents += "#include <nlohmann/json.hpp>\n"
+ contents += "\n"
+ contents += "class TestSettingsJSONSerialization : public QObject {\n"
+ contents += "\tQ_OBJECT\n"
+ contents += "\t" + settingsClassName + " createSettingsInstance() const {\n"
+ contents += "\t\t" + settingsClassName + " settings;\n"
+ for fieldName in settingsFields:
+ if fieldName in excludeFields:
+ continue
+ if settingsFields[fieldName] == "OverlaySettings":
+ for overlayField in structs["OverlaySettings"]:
+ contents += "\t\tsettings." + fieldName + "." + overlayField + " = "\
+ + getDefaultValueForType(structs["OverlaySettings"][overlayField]) + ";"
+ elif settingsFields[fieldName] == "bool":
+ # For boolean values, we simply use the inverse of whatever the default is
+ contents += "\t\tsettings." + fieldName + " = !settings." + fieldName + ";\n"
+ else:
+ contents += "\t\tsettings." + fieldName + " = " + getDefaultValueForType(settingsFields[fieldName]) + ";\n"
+ contents += "\n"
+ contents += "\t\treturn settings;\n"
+ contents += "\t}\n"
+ contents += "\n"
+
+ contents += "private slots:\n"
+ contents += "\tvoid noDefaultValuesMatched() {\n"
+ contents += "\t\tconst " + settingsClassName + " defaults;\n"
+ contents += "\t\tconst " + settingsClassName + " myInstance = createSettingsInstance();\n"
+ for fieldName in settingsFields:
+ if fieldName in excludeFields:
+ continue
+ contents += "\t\tQVERIFY2(defaults." + fieldName + "!= myInstance." + fieldName + ", \"Field '" \
+ + fieldName + "' was set to its default value (breaking underlying assumption of the following test)\");\n"
+ contents += "\t}\n"
+ contents += "\n"
+ contents += "\tvoid testJSONSerialization() {\n"
+ contents += "\t\tconst " + settingsClassName + " original = createSettingsInstance();\n"
+ contents += "\t\tnlohmann::json jsonRepresentation = original;\n"
+ contents += "\t\tconst " + settingsClassName + " deserialized = jsonRepresentation;\n"
+ contents += "\n"
+ for fieldName in settingsFields:
+ if fieldName in excludeFields:
+ continue
+ else:
+ contents += "\t\tQCOMPARE(original." + fieldName + ", deserialized." + fieldName + ");\n"
+ contents += "\t}\n"
+ contents += "};\n"
+ contents += "\n"
+ contents += "QTEST_MAIN(TestSettingsJSONSerialization)\n"
+ contents += "#include \"TestSettingsJSONSerialization.moc\"\n"
+
+ return contents
+
+
+def generateDefaultConstruction(structName, fields):
+ contents = structName + "{"
+ for currentField in fields:
+ contents += "/*" + currentField + "*/ " + getDefaultValueForType(fields[currentField]) + ","
+
+ if len(fields) > 0:
+ # remove trailing comma
+ contents = contents[:-1]
+
+ return contents + "}"
+
+
+
+def main():
+ parser = argparse.ArgumentParser("Generates the test case for the JSON (de)serialization of the settings struct")
+ parser.add_argument("--settings-header", help="The path to the header file containing the definition of the Settings struct", metavar="PATH",
+ required = True)
+ parser.add_argument("--settings-struct-name", help="The name of the settings struct", default="Settings")
+ parser.add_argument("--ignore-fields", help="A comma-separated list of fields in the settings struct to exclude from the test", default="kpCertificate")
+ parser.add_argument("--output-file", help="Path to where the output shall be written. If none is given, the result is written to stdout", metavar="PATH")
+
+ args = parser.parse_args()
+
+ headerContents = open(args.settings_header, "r").read()
+
+ for currentMatch in re.finditer(r"struct\s*(\w+)\s\{", headerContents):
+ definition = extractClassDefinition(currentMatch.group(1), headerContents[currentMatch.start() : ], classIdentifier="struct")
+
+ fields = extractFields(definition)
+
+ defaultValues[currentMatch.group(1)] = generateDefaultConstruction(currentMatch.group(1), fields)
+
+ structs[currentMatch.group(1)] = fields
+
+ ignoredFields = args.ignore_fields.split(",")
+ ignoredFields = [x.strip() for x in ignoredFields]
+
+ contents = generateTestBody(structs[args.settings_struct_name], args.settings_struct_name, excludeFields=ignoredFields)
+
+ if args.output_file:
+ outFile = open(args.output_file, "w")
+ outFile.write(contents)
+ else:
+ print(contents)
+
+
+
+
+if __name__ == "__main__":
+ main()