From b63c1f6ce75d82028efc364cff8277c77854dcc3 Mon Sep 17 00:00:00 2001
From: Tobias Hunger <tobias.hunger@qt.io>
Date: Tue, 13 Sep 2016 10:56:42 +0200
Subject: cmake-server: Add unit test

---
 Tests/CMakeLists.txt           |   9 +++
 Tests/Server/CMakeLists.txt    |  23 ++++++++
 Tests/Server/cmakelib.py       | 126 +++++++++++++++++++++++++++++++++++++++++
 Tests/Server/empty.cpp         |   5 ++
 Tests/Server/server-test.py    |  82 +++++++++++++++++++++++++++
 Tests/Server/tc_handshake.json |  71 +++++++++++++++++++++++
 6 files changed, 316 insertions(+)
 create mode 100644 Tests/Server/CMakeLists.txt
 create mode 100644 Tests/Server/cmakelib.py
 create mode 100644 Tests/Server/empty.cpp
 create mode 100644 Tests/Server/server-test.py
 create mode 100644 Tests/Server/tc_handshake.json

diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt
index 97770ed..8cf1faa 100644
--- a/Tests/CMakeLists.txt
+++ b/Tests/CMakeLists.txt
@@ -2722,6 +2722,15 @@ ${CMake_BINARY_DIR}/bin/cmake -DDIR=dev -P ${CMake_SOURCE_DIR}/Utilities/Release
   ADD_TEST_MACRO(CMakeCommands.target_compile_definitions target_compile_definitions)
   ADD_TEST_MACRO(CMakeCommands.target_compile_options target_compile_options)
 
+  if(CMake_HAVE_SERVER_MODE)
+    # The cmake server-mode test requires python for a simple client.
+    find_package(PythonInterp QUIET)
+    if(PYTHON_EXECUTABLE)
+      set(Server_BUILD_OPTIONS -DPYTHON_EXECUTABLE:FILEPATH=${PYTHON_EXECUTABLE})
+      ADD_TEST_MACRO(Server Server)
+    endif()
+  endif()
+
   configure_file(
     "${CMake_SOURCE_DIR}/Tests/CTestTestCrash/test.cmake.in"
     "${CMake_BINARY_DIR}/Tests/CTestTestCrash/test.cmake"
diff --git a/Tests/Server/CMakeLists.txt b/Tests/Server/CMakeLists.txt
new file mode 100644
index 0000000..8daf12a
--- /dev/null
+++ b/Tests/Server/CMakeLists.txt
@@ -0,0 +1,23 @@
+cmake_minimum_required(VERSION 3.4)
+project(Server CXX)
+
+find_package(PythonInterp REQUIRED)
+
+macro(do_test bsname file)
+  execute_process(COMMAND ${PYTHON_EXECUTABLE}
+    "${CMAKE_SOURCE_DIR}/server-test.py"
+    "${CMAKE_COMMAND}"
+    "${CMAKE_SOURCE_DIR}/${file}"
+    "${CMAKE_SOURCE_DIR}"
+    "${CMAKE_BINARY_DIR}"
+    RESULT_VARIABLE test_result
+    )
+
+  if (NOT test_result EQUAL 0)
+    message(SEND_ERROR "TEST FAILED")
+  endif()
+endmacro()
+
+do_test("test_handshake" "tc_handshake.json")
+
+add_executable(Server empty.cpp)
diff --git a/Tests/Server/cmakelib.py b/Tests/Server/cmakelib.py
new file mode 100644
index 0000000..48ebc89
--- /dev/null
+++ b/Tests/Server/cmakelib.py
@@ -0,0 +1,126 @@
+import sys, subprocess, json
+
+termwidth = 150
+
+print_communication = True
+
+def ordered(obj):
+  if isinstance(obj, dict):
+    return sorted((k, ordered(v)) for k, v in obj.items())
+  if isinstance(obj, list):
+    return sorted(ordered(x) for x in obj)
+  else:
+    return obj
+
+def col_print(title, array):
+  print
+  print
+  print(title)
+
+  indentwidth = 4
+  indent = " " * indentwidth
+
+  if not array:
+    print(indent + "<None>")
+    return
+
+  padwidth = 2
+
+  maxitemwidth = len(max(array, key=len))
+
+  numCols = max(1, int((termwidth - indentwidth + padwidth) / (maxitemwidth + padwidth)))
+
+  numRows = len(array) // numCols + 1
+
+  pad = " " * padwidth
+
+  for index in range(numRows):
+    print(indent + pad.join(item.ljust(maxitemwidth) for item in array[index::numRows]))
+
+def waitForRawMessage(cmakeCommand):
+  stdoutdata = ""
+  payload = ""
+  while not cmakeCommand.poll():
+    stdoutdataLine = cmakeCommand.stdout.readline()
+    if stdoutdataLine:
+      stdoutdata += stdoutdataLine.decode('utf-8')
+    else:
+      break
+    begin = stdoutdata.find("[== CMake Server ==[\n")
+    end = stdoutdata.find("]== CMake Server ==]")
+
+    if (begin != -1 and end != -1):
+      begin += len("[== CMake Server ==[\n")
+      payload = stdoutdata[begin:end]
+      if print_communication:
+        print("\nSERVER>", json.loads(payload), "\n")
+      return json.loads(payload)
+
+def writeRawData(cmakeCommand, content):
+  writeRawData.counter += 1
+  payload = """
+[== CMake Server ==[
+%s
+]== CMake Server ==]
+""" % content
+
+  rn = ( writeRawData.counter % 2 ) == 0
+
+  if rn:
+    payload = payload.replace('\n', '\r\n')
+
+  if print_communication:
+    print("\nCLIENT>", content, "(Use \\r\\n:", rn, ")\n")
+  cmakeCommand.stdin.write(payload.encode('utf-8'))
+  cmakeCommand.stdin.flush()
+writeRawData.counter = 0
+
+def writePayload(cmakeCommand, obj):
+  writeRawData(cmakeCommand, json.dumps(obj))
+
+def initProc(cmakeCommand):
+  cmakeCommand = subprocess.Popen([cmakeCommand, "-E", "server"],
+                                  stdin=subprocess.PIPE,
+                                  stdout=subprocess.PIPE)
+
+  packet = waitForRawMessage(cmakeCommand)
+  if packet == None:
+    print("Not in server mode")
+    sys.exit(1)
+
+  if packet['type'] != 'hello':
+    print("No hello message")
+    sys.exit(1)
+
+  return cmakeCommand
+
+def waitForMessage(cmakeCommand, expected):
+  data = ordered(expected)
+  packet = ordered(waitForRawMessage(cmakeCommand))
+
+  if packet != data:
+    sys.exit(-1)
+  return packet
+
+def waitForReply(cmakeCommand, originalType, cookie):
+  packet = waitForRawMessage(cmakeCommand)
+  if packet['cookie'] != cookie or packet['type'] != 'reply' or packet['inReplyTo'] != originalType:
+    sys.exit(1)
+
+def waitForError(cmakeCommand, originalType, cookie, message):
+  packet = waitForRawMessage(cmakeCommand)
+  if packet['cookie'] != cookie or packet['type'] != 'error' or packet['inReplyTo'] != originalType or packet['errorMessage'] != message:
+    sys.exit(1)
+
+def waitForProgress(cmakeCommand, originalType, cookie, current, message):
+  packet = waitForRawMessage(cmakeCommand)
+  if packet['cookie'] != cookie or packet['type'] != 'progress' or packet['inReplyTo'] != originalType or packet['progressCurrent'] != current or packet['progressMessage'] != message:
+    sys.exit(1)
+
+def handshake(cmakeCommand, major, minor):
+  version = { 'major': major }
+  if minor >= 0:
+    version['minor'] = minor
+
+  writePayload(cmakeCommand, { 'type': 'handshake', 'protocolVersion': version, 'cookie': 'TEST_HANDSHAKE' })
+  waitForReply(cmakeCommand, 'handshake', 'TEST_HANDSHAKE')
diff --git a/Tests/Server/empty.cpp b/Tests/Server/empty.cpp
new file mode 100644
index 0000000..766b775
--- /dev/null
+++ b/Tests/Server/empty.cpp
@@ -0,0 +1,5 @@
+
+int main()
+{
+  return 0;
+}
diff --git a/Tests/Server/server-test.py b/Tests/Server/server-test.py
new file mode 100644
index 0000000..e0a3b3b
--- /dev/null
+++ b/Tests/Server/server-test.py
@@ -0,0 +1,82 @@
+import sys, cmakelib, json
+
+debug = True
+
+cmakeCommand = sys.argv[1]
+testFile = sys.argv[2]
+sourceDir = sys.argv[3]
+buildDir = sys.argv[4]
+
+print("SourceDir: ", sourceDir, " -- BuildDir: ", buildDir)
+
+proc = cmakelib.initProc(cmakeCommand)
+
+with open(testFile) as f:
+    testText = f.read()
+    testText = testText.replace('%BUILDDIR%', buildDir)
+    testText = testText.replace('%SOURCEDIR%', sourceDir)
+    testData = json.loads(testText)
+
+buildDir = sys.argv[3]
+sourceDir = sys.argv[4]
+
+for obj in testData:
+    if 'sendRaw' in obj:
+        data = obj['sendRaw']
+        if debug: print("Sending raw:", data)
+        cmakelib.writeRawData(proc, data)
+    elif 'send' in obj:
+        data = obj['send']
+        if debug: print("Sending:", json.dumps(data))
+        cmakelib.writePayload(proc, data)
+    elif 'recv' in obj:
+        data = obj['recv']
+        if debug: print("Waiting for:", json.dumps(data))
+        cmakelib.waitForMessage(proc, data)
+    elif 'reply' in obj:
+        data = obj['reply']
+        if debug: print("Waiting for reply:", json.dumps(data))
+        originalType = ""
+        cookie = ""
+        if 'cookie' in data: cookie = data['cookie']
+        if 'type' in data: originalType = data['type']
+        cmakelib.waitForReply(proc, originalType, cookie)
+    elif 'error' in obj:
+        data = obj['error']
+        if debug: print("Waiting for error:", json.dumps(data))
+        originalType = ""
+        cookie = ""
+        message = ""
+        if 'cookie' in data: cookie = data['cookie']
+        if 'type' in data: originalType = data['type']
+        if 'message' in data: message = data['message']
+        cmakelib.waitForError(proc, originalType, cookie, message)
+    elif 'progress' in obj:
+        data = obj['progress']
+        if debug: print("Waiting for progress:", json.dumps(data))
+        originalType = ''
+        cookie = ""
+        current = 0
+        message = ""
+        if 'cookie' in data: cookie = data['cookie']
+        if 'type' in data: originalType = data['type']
+        if 'current' in data: current = data['current']
+        if 'message' in data: message = data['message']
+        cmakelib.waitForProgress(proc, originalType, cookie, current, message)
+    elif 'handshake' in obj:
+        data = obj['handshake']
+        if debug: print("Doing handshake:", json.dumps(data))
+        major = -1
+        minor = -1
+        if 'major' in data: major = data['major']
+        if 'minor' in data: minor = data['minor']
+        cmakelib.handshake(proc, major, minor)
+    elif 'message' in obj:
+        print("MESSAGE:", obj["message"])
+    else:
+        print("Unknown command:", json.dumps(obj))
+        sys.exit(2)
+
+    print("Completed")
+
+sys.exit(0)
diff --git a/Tests/Server/tc_handshake.json b/Tests/Server/tc_handshake.json
new file mode 100644
index 0000000..5261581
--- /dev/null
+++ b/Tests/Server/tc_handshake.json
@@ -0,0 +1,71 @@
+[
+{ "message": "Testing basic message handling:" },
+
+{ "sendRaw": "Sometext"},
+{ "recv": {"cookie":"","errorMessage":"Failed to parse JSON input.","inReplyTo":"","type":"error"} },
+
+{ "message": "Testing invalid json input"},
+{ "send": { "test": "sometext" } },
+{ "recv": {"cookie":"","errorMessage":"No type given in request.","inReplyTo":"","type":"error"} },
+
+{ "send": {"test": "sometext","cookie":"monster"} },
+{ "recv": {"cookie":"monster","errorMessage":"No type given in request.","inReplyTo":"","type":"error"} },
+
+{ "message": "Testing handshake" },
+{ "send": {"type": "sometype","cookie":"monster2"} },
+{ "recv": {"cookie":"monster2","errorMessage":"Waiting for type \"handshake\".","inReplyTo":"sometype","type":"error"} },
+
+{ "send": {"type": "handshake"} },
+{ "recv": {"cookie":"","errorMessage":"\"protocolVersion\" is required for \"handshake\".","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","foo":"bar"} },
+{ "recv": {"cookie":"","errorMessage":"\"protocolVersion\" is required for \"handshake\".","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","protocolVersion":"bar"} },
+{ "recv": {"cookie":"","errorMessage":"\"protocolVersion\" must be a JSON object.","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","protocolVersion":{}} },
+{ "recv": {"cookie":"","errorMessage":"\"major\" must be set and an integer.","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","protocolVersion":{"major":"foo"}} },
+{ "recv": {"cookie":"","errorMessage":"\"major\" must be set and an integer.","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","protocolVersion":{"major":1, "minor":"foo"}} },
+{ "recv": {"cookie":"","errorMessage":"\"minor\" must be unset or an integer.","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","protocolVersion":{"major":-1, "minor":-1}} },
+{ "recv": {"cookie":"","errorMessage":"\"major\" must be >= 0.","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","protocolVersion":{"major":10, "minor":-1}} },
+{ "recv": {"cookie":"","errorMessage":"\"minor\" must be >= 0 when set.","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","protocolVersion":{"major":10000}} },
+{ "recv": {"cookie":"","errorMessage":"Protocol version not supported.","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"type": "handshake","protocolVersion":{"major":1, "minor":10000}} },
+{ "recv": {"cookie":"","errorMessage":"Protocol version not supported.","inReplyTo":"handshake","type":"error"} },
+
+{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1}} },
+{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: \"buildDirectory\" is missing."} },
+
+{ "message": "Testing protocol version specific options (1.0):" },
+{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":"/tmp/src"} },
+{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: \"buildDirectory\" is missing."} },
+
+{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":"/tmp/src","buildDirectory":"/tmp/build"} },
+{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: \"sourceDirectory\" is not a directory."} },
+
+{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":".","buildDirectory":"/tmp/build","extraGenerator":"CodeBlocks"} },
+{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: \"generator\" is unset but required."} },
+
+{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":".","buildDirectory":"/tmp/build","generator":"XXXX","extraGenerator":"CodeBlocks"} },
+{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: Could not set up the requested combination of \"generator\" and \"extraGenerator\""} },
+
+{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":".","buildDirectory":"/tmp/build","generator":"Ninja","extraGenerator":"XXXX"} },
+{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"error","errorMessage":"Failed to activate protocol version: Could not set up the requested combination of \"generator\" and \"extraGenerator\""} },
+
+{ "send": {"cookie":"zimtstern","type": "handshake","protocolVersion":{"major":1},"sourceDirectory":".","buildDirectory":"/tmp/build","generator":"Ninja","extraGenerator":"CodeBlocks"} },
+{ "recv": {"cookie":"zimtstern","inReplyTo":"handshake","type":"reply"} },
+
+{ "message": "Everything ok." }
+]
-- 
cgit v0.12