diff --git a/.circleci/SideBySide/config.json b/.circleci/SideBySide/config.json index 73f330f26..8206e610a 100644 --- a/.circleci/SideBySide/config.json +++ b/.circleci/SideBySide/config.json @@ -1,6 +1,6 @@ { "Data": { - "ConnectionString": "server=127.0.0.1;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest", + "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest", "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog" } } diff --git a/.circleci/config.yml b/.circleci/config.yml index 70aacc167..ba36c9d6d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,9 +4,12 @@ parameters: type: string default: "1.0.0" +orbs: + win: circleci/windows@2.4.0 + commands: setup-environment-ubuntu: - description: "Setup the linux environment" + description: Setup Linux environment steps: - run: name: Install .NET Core 6.0 @@ -20,8 +23,52 @@ commands: sudo apt-get install -y mariadb-client-core-10.3 sudo apt-get install -y dotnet-sdk-6.0 dotnet --info + run-tests-win: + description: Run tests on Windows + parameters: + target_framework: + type: string + steps: + - run: + name: Unit tests + command: | + cd tests\MySqlConnector.Tests + dotnet.exe test -f << parameters.target_framework >> -c Release --no-build + cd ..\.. + - run: + name: Conformance tests + command: | + cd tests/Conformance.Tests/ + dotnet.exe test -f << parameters.target_framework >> -c Release --no-build + cd ..\.. + - run: + name: SideBySide tests + command: | + cd tests\SideBySide + dotnet.exe test -f << parameters.target_framework >> -c Release --no-build + cd ..\.. jobs: + test-windows: + executor: win/default + steps: + - checkout + - run: + name: Build project binaries + command: | + choco upgrade dotnet-sdk + dotnet.exe build -c Release + - run: + name: Start SingleStore for SideBySide tests + command: | + pip install pymysql + python.exe .circleci\s2ms_cluster.py start singlestoretest + - run: + name: Fill test config + command: python.exe .circleci\fill_test_config.py + - run-tests-win: + target_framework: net6.0 + test-ubuntu: parameters: singlestore_image: @@ -46,25 +93,27 @@ jobs: name: Copy config file for SideBySide tests command: | cp ./.circleci/SideBySide/config.json tests/SideBySide/config.json + sed -i "s|SINGLESTORE_HOST|127.0.0.1|g" tests/SideBySide/config.json sed -i "s|SQL_USER_PASSWORD|${SQL_USER_PASSWORD}|g" tests/SideBySide/config.json sed -i "s|SQL_USER_NAME|root|g" tests/SideBySide/config.json + cp tests/SideBySide/config.json tests/SideBySide/bin/Release/net6.0/config.json - run: name: Unit tests command: | cd tests/MySqlConnector.Tests - dotnet test -f net6.0 -c Release + dotnet test -f net6.0 -c Release --no-build cd ../../ - run: name: Conformance tests command: | cd tests/Conformance.Tests/ - dotnet test -f net6.0 -c Release + dotnet test -f net6.0 -c Release --no-build cd ../../ - run: name: SideBySide tests command: | cd tests/SideBySide - dotnet test -f net6.0 -c Release + dotnet test -f net6.0 -c Release --no-build cd ../../ workflows: @@ -89,3 +138,5 @@ workflows: parameters: singlestore_image: - singlestore/cluster-in-a-box:centos-7.3.13-761e3259b3-3.2.11-1.11.9 + - test-windows: + name: Test S2MS on Windows diff --git a/.circleci/fill_test_config.py b/.circleci/fill_test_config.py new file mode 100644 index 000000000..b237e0d48 --- /dev/null +++ b/.circleci/fill_test_config.py @@ -0,0 +1,35 @@ +import json +import os + + +CLUSTER_ID_FILE = "CLUSTER_ID" +HOSTNAME_TMPL = "svc-{}-ddl.aws-frankfurt-1.svc.singlestore.com" + +NET_FRAMEWORKS = ["net452", "net461", "net472", "netcoreapp3.1", "net5.0", "net6.0"] + + +if __name__ == "__main__": + + home_dir = os.getenv("HOMEPATH") + if home_dir is None: + home_dir = os.getenv("HOME") + + with open(CLUSTER_ID_FILE, "r") as f: + cluster_id = f.read().strip() + + hostname = HOSTNAME_TMPL.format(cluster_id) + password = os.getenv("SQL_USER_PASSWORD") + + with open("./.circleci/SideBySide/config.json", "r") as f_in: + config_content = f_in.read() + + config_content = config_content.replace("SINGLESTORE_HOST", hostname, 1) + config_content = config_content.replace("SQL_USER_PASSWORD", password, 1) + config_content = config_content.replace("SQL_USER_NAME", "admin", 1) + + for target_framework in NET_FRAMEWORKS: + with open(f"tests/SideBySide/bin/Release/{target_framework}/config.json", "w") as f_out: + f_out.write(config_content) + + with open(os.path.join(home_dir, "CONNECTION_STRING"), "w") as f_conn: + f_conn.write(json.loads(config_content)["Data"]["ConnectionString"]) diff --git a/.circleci/s2ms_cluster.py b/.circleci/s2ms_cluster.py new file mode 100644 index 000000000..3db3a5940 --- /dev/null +++ b/.circleci/s2ms_cluster.py @@ -0,0 +1,134 @@ +import json +import os +import pymysql +import requests +from requests.adapters import HTTPAdapter +import sys +from time import sleep +from typing import Dict, Optional +from urllib3 import Retry + +BASE_URL = "https://api.singlestore.com" +CLUSTERS_PATH = "/v0beta/clusters" + +SQL_USER_PASSWORD = os.getenv("SQL_USER_PASSWORD") # project UI env-var reference +S2MS_API_KEY = os.getenv("S2MS_API_KEY") # project UI env-var reference + +HEADERS = { + "Authorization": f"Bearer {S2MS_API_KEY}", + "Content-Type": "application/json", + "Accept": "application/json" +} + +CLUSTER_NAME = ".NET-connector-ci-test-cluster" +AWS_EU_CENTRAL_REGION = "7e7ffd27-20f7-44b6-87e6-e72828a81ac7" +AUTO_TERMINATE_MINUTES = 30 + +PAYLOAD_FOR_CREATE = { + "name": CLUSTER_NAME, + "regionID": AWS_EU_CENTRAL_REGION, + "adminPassword": SQL_USER_PASSWORD, + "expiresAt": f"{AUTO_TERMINATE_MINUTES}m", + "firewallRanges": [ + "0.0.0.0/0" + ], + "size": "S-00" +} +HOSTNAME_TMPL = "svc-{}-ddl.aws-frankfurt-1.svc.singlestore.com" +CLUSTER_ID_FILE = "CLUSTER_ID" + +TOTAL_RETRIES = 5 +S2MS_REQUEST_TIMEOUT = 60 + + +def request_with_retry(request_method, url, data=None, headers=HEADERS): + try: + with requests.Session() as s: + retries = Retry( + total=TOTAL_RETRIES, + backoff_factor=0.2, + status_forcelist=[500, 502, 503, 504]) + + s.mount('http://', HTTPAdapter(max_retries=retries)) + s.mount('https://', HTTPAdapter(max_retries=retries)) + + return s.request(request_method, url, data=data, headers=headers, timeout=S2MS_REQUEST_TIMEOUT) + except requests.exceptions.RequestException as e: + raise SystemExit(e) + + +def create_cluster() -> str: + cl_id = request_with_retry("POST", BASE_URL + CLUSTERS_PATH, data=json.dumps(PAYLOAD_FOR_CREATE)) + return cl_id.json()["clusterID"] + + +def get_cluster_info(cluster_id: str) -> Dict: + cl_id = request_with_retry("GET", BASE_URL + CLUSTERS_PATH + f"/{cluster_id}") + return cl_id.json() + + +def is_cluster_active(cluster_id: str) -> bool: + cl_info = get_cluster_info(cluster_id) + return cl_info["state"] == "Active" + + +def wait_start(cluster_id: str) -> None: + print(f"Waiting for cluster {cluster_id} to be available for connection..", end="", flush=True) + time_wait = 0 + while (not is_cluster_active(cluster_id) and time_wait < 600): + print(".", end="", flush=True) + sleep(5) + time_wait += 5 + if time_wait < 600: + print("\nCluster is active!") + else: + print(f"\nTimeout error: can't connect to {cluster_id} for more than 10 minutes!") + + +def terminate_cluster(cluster_id: str) -> None: + request_with_retry("DELETE", BASE_URL + CLUSTERS_PATH + f"/{cluster_id}") + + +def check_connection(cluster_id: str, create_db: Optional[str] = None): + conn = pymysql.connect( + user="admin", + password=SQL_USER_PASSWORD, + host=HOSTNAME_TMPL.format(cluster_id), + port=3306) + + cur = conn.cursor() + try: + cur.execute("SELECT NOW():>TEXT") + res = cur.fetchall() + print(f"Successfully connected to {cluster_id} at {res[0][0]}") + + if create_db is not None: + cur.execute(f"DROP DATABASE IF EXISTS {create_db}") + cur.execute(f"CREATE DATABASE {create_db}") + finally: + cur.close() + conn.close() + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Not enough arguments to start/terminate cluster!") + exit(1) + command = sys.argv[1] + db_name = None + if len(sys.argv) > 2: + db_name = sys.argv[2] + + if command == "start": + new_cl_id = create_cluster() + with open(CLUSTER_ID_FILE, "w") as f: + f.write(new_cl_id) + wait_start(new_cl_id) + check_connection(new_cl_id, db_name) + exit(0) + + if command == "terminate": + with open(CLUSTER_ID_FILE, "r") as f: + cl_id = f.read() + terminate_cluster(cl_id) + exit(0) diff --git a/.circleci/setup_cluster.sh b/.circleci/setup_cluster.sh index cd5e57d09..a76458fba 100755 --- a/.circleci/setup_cluster.sh +++ b/.circleci/setup_cluster.sh @@ -36,6 +36,7 @@ singlestore-wait-start() { echo -n "." sleep 0.2 done + mysql -u root -h 127.0.0.1 -P 3306 -p"${SQL_USER_PASSWORD}" -e "create database if not exists singlestoretest" >/dev/null 2>/dev/null echo ". Success!" } diff --git a/tests/Conformance.Tests/DbFactoryFixture.cs b/tests/Conformance.Tests/DbFactoryFixture.cs index db9468bde..9f2320e9e 100644 --- a/tests/Conformance.Tests/DbFactoryFixture.cs +++ b/tests/Conformance.Tests/DbFactoryFixture.cs @@ -10,7 +10,21 @@ public class DbFactoryFixture : IDbFactoryFixture public DbFactoryFixture() { String sqlUserPassword = Environment.GetEnvironmentVariable("SQL_USER_PASSWORD") ?? "pass"; - ConnectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING") ?? String.Format("Server=localhost;User Id=root;Password={0};SSL Mode=None", sqlUserPassword); + + String home = Environment.GetEnvironmentVariable("HOMEPATH") ?? "~"; + String connectionStringFile = System.IO.Path.Join(home, "CONNECTION_STRING"); + + string connectionString; + try + { + connectionString = System.IO.File.ReadAllText(connectionStringFile); + } + catch (System.Exception) + { + connectionString = ""; + } + + ConnectionString = connectionString.Length > 0 ? connectionString : String.Format("Server=localhost;Port=3306;User Id=root;Password={0};SSL Mode=None", sqlUserPassword); } public string ConnectionString { get; }