From 85f8778cb65d977c426c0922a899990d6580fdd7 Mon Sep 17 00:00:00 2001 From: Hannah Adams <67337692+hfadams@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:23:52 -0230 Subject: [PATCH] adding testing suite --- .gitignore | 5 +- test-build.sh | 13 ++ testing/metadata.xml | 6 + testing/reinstallPackage.py | 56 +++++++ testing/testTemplateLibrary.py | 295 +++++++++++++++++++++++++++++++++ 5 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 test-build.sh create mode 100644 testing/metadata.xml create mode 100644 testing/reinstallPackage.py create mode 100644 testing/testTemplateLibrary.py diff --git a/.gitignore b/.gitignore index afaca59..8d86abd 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ vignettes/*.pdf .Renviron # Visual studio -*.vs/ \ No newline at end of file +*.vs/ + +# testing +scratch/ \ No newline at end of file diff --git a/test-build.sh b/test-build.sh new file mode 100644 index 0000000..f7b5eff --- /dev/null +++ b/test-build.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + + +python testing/reinstallPackage.py \ + --meta "testing/metadata.xml" \ + --folder ./src \ + --reinstall \ + --packagemanager "/c/Program Files/SyncroSim/SyncroSim.PackageManager.exe" + + + +python testing/testTemplateLibrary.py testing/metadata.xml --console "/c/Program Files/SyncroSim/SyncroSim.Console.exe" --tempdir scratch/ \ No newline at end of file diff --git a/testing/metadata.xml b/testing/metadata.xml new file mode 100644 index 0000000..b9f6fc9 --- /dev/null +++ b/testing/metadata.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/testing/reinstallPackage.py b/testing/reinstallPackage.py new file mode 100644 index 0000000..137a162 --- /dev/null +++ b/testing/reinstallPackage.py @@ -0,0 +1,56 @@ +import xml.etree.ElementTree as ET +import subprocess +import argparse +import os + +def reinstall_package_from_folder(console_path, metadata_path, folder_path): + # Step 1: Parse metadata.xml to find package name + try: + tree = ET.parse(metadata_path) + root = tree.getroot() + package_name = root.attrib["name"] + print(f"๐Ÿ“ฆ Package to reinstall: {package_name}") + except Exception as e: + print(f"โŒ Failed to parse metadata: {e}") + return + + # Step 2: Uninstall the package + print(f"๐Ÿงน Uninstalling package: {package_name}") + uninstall_cmd = [console_path, f"--removeall={package_name}"] + result = subprocess.run(uninstall_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"โš ๏ธ Uninstall failed or package not found: {result.stderr.strip()}") + else: + print("โœ… Package uninstalled successfully.") + + # Step 3: Install the package from folder + print(f"๐Ÿ“‚ Installing package from folder") + xinstall_cmd = [console_path, f"--xinstall={folder_path}", "--force"] + result = subprocess.run(xinstall_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"โŒ Install from folder failed: {result.stderr.strip()}") + else: + print("โœ… Package installed successfully from folder.") + + + +def main(): + parser = argparse.ArgumentParser(description="SyncroSim Package Utilities") + + parser.add_argument("--meta", required=True, help="Path to meta-data.xml") + parser.add_argument("--packagemanager", default="SyncroSim.PackageManager.exe", help="Path to SyncroSim Package Manager") + parser.add_argument("--reinstall", action="store_true", help="Reinstall package using metadata and --folder path") + parser.add_argument("--folder", help="Folder path to use with --reinstall") + + args = parser.parse_args() + + if args.reinstall: + if not args.folder: + print("โŒ You must provide --folder when using --reinstall") + return + reinstall_package_from_folder(args.packagemanager, args.meta, args.folder) + + + +if __name__ == "__main__": + main() diff --git a/testing/testTemplateLibrary.py b/testing/testTemplateLibrary.py new file mode 100644 index 0000000..f8f54b6 --- /dev/null +++ b/testing/testTemplateLibrary.py @@ -0,0 +1,295 @@ +import xml.etree.ElementTree as ET +import subprocess +import os +import argparse +from urllib.parse import urlparse +import re +import urllib.request +import tempfile +import zipfile +import time + + + +def update_metadata(file_path): + tree = ET.parse(file_path) + root = tree.getroot() + + # Check if template library exists + for lib in root.findall("onlineLibrary"): + name = lib.attrib["name"] + print(f" โ†ช Found template library: {name}") + + tree.write(file_path, encoding="utf-8", xml_declaration=True) + return tree, root + +def get_package_info_from_metadata(metadata_path): + tree = ET.parse(metadata_path) + root = tree.getroot() + package_name = root.attrib["name"] + package_version = root.attrib["version"] + return package_name, package_version + + +def run_console_command(args): + result = subprocess.run(args, capture_output=True, text=True) + print(result.stdout) + if result.returncode != 0: + print("โŒ Console error:", result.stderr) + return False + return True + +def get_installed_package_version(console_path, lib_path, package_name): + import re + + result = subprocess.run( + [console_path, "--list", "--packages", f"--lib={lib_path}"], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print("โŒ Failed to list packages:", result.stderr) + return None + + lines = result.stdout.splitlines() + + # Step 1: Find the second header (Version, Schema, Status) + header_idx = None + for i, line in enumerate(lines): + if "Version" in line and "Status" in line: + header_idx = i + break + + if header_idx is None: + print("โš ๏ธ Could not find expected package column headers.") + return None + + data_lines = lines[header_idx + 1:] + + for line in data_lines: + if not line.strip(): + continue + + # Extract the first non-empty token as the package name + tokens = re.split(r"\s{2,}", line.strip()) # split on double+ spaces + if not tokens: + continue + + name = tokens[0].strip() + version = tokens[1].strip() if len(tokens) > 1 else "" + + print(f"๐Ÿ” Checking line โ†’ Name: {name}, Version: {version}") + + if name == package_name: + return version + + print(f"โš ๏ธ Package {package_name} not found in list.") + return None + + +def sync_library_package_version(console_path, metadata_path, lib_path): + package_name, expected_version = get_package_info_from_metadata(metadata_path) + print(f"๐Ÿ“ฆ Expected package: {package_name} v{expected_version}") + + installed_version = get_installed_package_version(console_path, lib_path, package_name) + if installed_version is None: + print(f"โš ๏ธ Package {package_name} is not installed in library.") + print("๐Ÿ“ฅ Adding correct version...") + subprocess.run([ + console_path, + "--add", "--package", + f"--pkg={package_name}", + f'ver="{expected_version}"', + f"--lib={lib_path}" + ]) + print(f"โœ… Package {package_name} v{expected_version} added.") + elif installed_version != expected_version: + + + print("๐Ÿ“ฅ Re-adding correct version...") + subprocess.run([ + console_path, + "--add", "--package", + f"--pkg={package_name}", + f"--ver={expected_version}", + f"--lib={lib_path}" + ]) + print(f"โœ… Package {package_name} v{expected_version} added.") + else: + print(f"โœ… Package {package_name} is up to date (v{installed_version}).") + + +def list_scenarios(console_path, lib_path, results_only=False): + result = subprocess.run( + [console_path, "--list", "--scenarios", f"--lib={lib_path}"], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print(f"โŒ Failed to list scenarios: {result.stderr}") + return [] + + lines = result.stdout.splitlines() + if len(lines) < 2: + print("โš ๏ธ No scenarios listed.") + return [] + + header = lines[0] + + # Get fixed column slices by header name + id_start = header.index("Id") + is_result_start = header.index("IsResult") + + # Estimate column widths by jumping to the next header or EOL + is_result_end = header.index("IsReadOnly") if "IsReadOnly" in header else None + id_end = header.index("ProjectId") if "ProjectId" in header else is_result_start + + scenario_ids = [] + + for line in lines[1:]: + id_str = line[id_start:id_end].strip() + is_result_str = line[is_result_start:is_result_end].strip().lower() + + if not id_str.isdigit(): + continue + + if results_only: + if is_result_str == "yes": + scenario_ids.append(id_str) + else: + if is_result_str == "no": + scenario_ids.append(id_str) + + label = "Result" if results_only else "Non-result" + print(f"๐Ÿ“‹ {label} scenario IDs: {', '.join(scenario_ids)}") + return scenario_ids + +def delete_scenarios(console_path, lib_path, scenario_ids): + if not scenario_ids: + print("โš ๏ธ No scenarios to delete.") + return + + for sid in scenario_ids: + print(f"๐Ÿ—‘๏ธ Deleting scenario ID: {sid}") + result = subprocess.run( + [console_path, "--delete", "--scenario", f"--sid={sid}", f"--lib={lib_path}", "--force"], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print(f"โŒ Failed to delete scenario {sid}: {result.stderr.strip()}") + else: + print(f"โœ… Deleted scenario {sid}") + + +def update_library(console_path, lib_path): + result = subprocess.run( + [console_path, "--update", f"--lib={lib_path}"], + capture_output=True, + text=True + ) + if result.returncode != 0: + print(f"โŒ Failed to update library: {result.stderr}") + return [] + + # Parse scenario IDs + sids = re.findall(r"\bID: (\d+)", result.stdout) + print("โœ… Library successfully updated.") + return sids + + + +def run_libraries(root, console_path, metadata_path, temp_dir=None): + for lib in root.findall("onlineLibrary"): + if temp_dir is None: + temp_dir = tempfile.gettempdir() + else: + os.makedirs(temp_dir, exist_ok=True) + + lib_name = lib.attrib["name"] + lib_url = lib.attrib["libraryLocation"] + parsed = urlparse(lib_url) + filename = os.path.basename(parsed.path) + + print(f"โฌ‡๏ธ Downloading: {filename}") + try: + zip_file_path = os.path.join(temp_dir, filename) + urllib.request.urlretrieve(lib_url, zip_file_path) + print(f"๐Ÿ“ฅ Saved to: {zip_file_path}") + except Exception as e: + print(f"โŒ Failed to download {filename}: {e}") + continue + + # Unzip the .ssimbak + extract_folder = os.path.join(temp_dir, f"{lib_name}_unzipped") + os.makedirs(extract_folder, exist_ok=True) + + try: + print(f"๐Ÿ—œ๏ธ Unzipping: {zip_file_path} โ†’ {extract_folder}") + with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: + zip_ref.extractall(extract_folder) + except zipfile.BadZipFile: + print(f"โŒ Invalid ZIP archive: {filename}") + continue + + # Find the .ssim + lib_path = next( + (os.path.join(extract_folder, f) for f in os.listdir(extract_folder) if f.endswith(".ssim")), + None + ) + if not lib_path: + print(f"โŒ No .ssim found in {extract_folder}") + continue + + ## update the library + update_library(console_path, lib_path) + + ## delete old results scenarios + result_sids = list_scenarios(console_path, lib_path, results_only=True) + delete_scenarios(console_path, lib_path, result_sids) + + # List scenario IDs + sids = list_scenarios(console_path, lib_path, results_only=False) + if not sids: + print(f"โš ๏ธ No scenarios found in {lib_path}") + continue + + print(f"\n๐Ÿ” Checking package versions in: {lib_path}") + sync_library_package_version(console_path, metadata_path, lib_path) + + # Run only the listed scenario IDs + sid_str = ",".join(sids) + print(f"โ–ถ๏ธ Running scenarios {sid_str} in: {lib_path}") + + start_time = time.time() + + result = subprocess.run( + [console_path, "--run", f"--lib={lib_path}", f"--sids={sid_str}"], + capture_output=True, + text=True + ) + + elapsed = time.time() - start_time + + if result.returncode != 0: + print(f"โŒ Scenario run failed after {elapsed:.2f} seconds:\n{result.stderr}") + else: + print(f"โœ… Scenario run completed in {elapsed:.2f} seconds:\n{result.stdout}") + + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("meta", help="Path to meta-data.xml") + parser.add_argument("--console", default="SyncroSim.Console.exe", help="Path to SyncroSim console") + parser.add_argument("--tempdir", default=None, help="Temporary directory") + args = parser.parse_args() + + tree, root = update_metadata(args.meta) + run_libraries(root, args.console, args.meta, args.tempdir) + +if __name__ == "__main__": + main()