"""
Script to port tests from apk-tools format to epkg YAML format.
Converts .test, .repo, and .installed files to YAML format.
"""
import os
import re
import yaml
from pathlib import Path
from typing import Dict, List, Optional, Tuple
SOURCE_DIR = Path("/c/package-managers/apk-tools/test/solver")
TARGET_DIR = Path("/c/epkg/tests/solver")
def parse_apk_index_file(file_path: Path) -> List[Dict]:
"""Parse an APK index format file (.repo or .installed) and return list of packages."""
packages = []
current_pkg = {}
with open(file_path, 'r') as f:
for line in f:
line = line.strip()
if not line:
if current_pkg:
packages.append(current_pkg)
current_pkg = {}
continue
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
if key == 'C':
continue
elif key == 'P':
current_pkg['pkgname'] = value
elif key == 'V':
current_pkg['version'] = value
elif key == 'A':
current_pkg['arch'] = value
elif key == 'S':
continue
elif key == 'I':
continue
elif key == 'D':
if 'requires' not in current_pkg:
current_pkg['requires'] = []
deps = value.split()
for dep in deps:
if dep.startswith('!'):
conflict = dep[1:]
if 'conflicts' not in current_pkg:
current_pkg['conflicts'] = []
current_pkg['conflicts'].append(conflict)
else:
current_pkg['requires'].append(dep)
elif key == 'p':
if 'provides' not in current_pkg:
current_pkg['provides'] = []
current_pkg['provides'].append(value)
elif key == 'k':
continue
elif key == 'i':
continue
if current_pkg:
packages.append(current_pkg)
return packages
def convert_package_to_yaml(pkg: Dict) -> Dict:
"""Convert a parsed package dict to YAML format."""
result = {
'pkgname': pkg.get('pkgname', ''),
'version': pkg.get('version', '1'),
'arch': pkg.get('arch', 'x86_64'),
}
if 'requires' in pkg and pkg['requires']:
result['requires'] = pkg['requires']
if 'conflicts' in pkg and pkg['conflicts']:
result['conflicts'] = pkg['conflicts']
arch = result['arch']
result['provides'] = []
if 'provides' in pkg and pkg['provides']:
for provide in pkg['provides']:
result['provides'].append(provide)
if '=' not in provide:
result['provides'].append(f"{provide}({arch}) = {result['version']}")
result['provides'].append(f"{result['pkgname']} = {result['version']}")
result['provides'].append(f"{result['pkgname']}({arch}) = {result['version']}")
return result
def parse_test_file(file_path: Path) -> Dict:
"""Parse a .test file and return test metadata."""
test_data = {
'args': '',
'repo': [],
'installed': None,
'world': None,
'expect': []
}
current_section = None
with open(file_path, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('@ARGS'):
test_data['args'] = line[5:].strip()
elif line.startswith('@REPO'):
repo_line = line[5:].strip()
parts = repo_line.split()
if len(parts) >= 2 and parts[0].startswith('@'):
tag = parts[0][1:]
repo_file = parts[1]
test_data['repo'].append((tag, repo_file))
else:
test_data['repo'].append((None, repo_line))
elif line.startswith('@CACHE'):
cache_line = line[6:].strip()
parts = cache_line.split()
if len(parts) >= 2 and parts[0].startswith('@'):
tag = parts[0][1:]
repo_file = parts[1]
test_data['repo'].append((tag, repo_file))
else:
test_data['repo'].append((None, cache_line))
elif line.startswith('@INSTALLED'):
test_data['installed'] = line[10:].strip()
elif line.startswith('@WORLD'):
test_data['world'] = line[7:].strip()
elif line.startswith('@EXPECT'):
current_section = 'expect'
elif current_section == 'expect' and line:
test_data['expect'].append(line)
return test_data
def clean_yaml_data(data):
"""Recursively clean YAML data by removing trivial/default values.
Removes:
- arch: x86_64 (default arch)
- depend_depth: 0 (default depth)
- install_time: 1000000000 (default time)
- ebin_exposure
- Empty arrays: rdepends: [], depends: [], ebin_links: []
- skip: false (default skip value)
- Converts empty dicts to None (for plan entries, so they output as `key:` instead of `key: {}`)
"""
if isinstance(data, dict):
cleaned = {}
for key, value in data.items():
if key == 'arch' and value == 'x86_64':
continue
if key == 'depend_depth' and value == 0:
continue
if key == 'install_time' and value == 1000000000:
continue
if key == 'ebin_exposure' and value in (True, False):
continue
if key == 'skip' and value is False:
continue
if isinstance(value, list) and len(value) == 0:
continue
cleaned_value = clean_yaml_data(value)
if cleaned_value == {}:
cleaned_value = None
if cleaned_value is None:
cleaned_value = {}
if cleaned_value != '':
if isinstance(cleaned_value, dict) or (isinstance(cleaned_value, list) and len(cleaned_value) > 0) or (not isinstance(cleaned_value, (list, dict)) and cleaned_value):
cleaned[key] = cleaned_value
return cleaned
elif isinstance(data, list):
return [clean_yaml_data(item) for item in data]
else:
return data
def convert_empty_dicts_to_none(data, in_plan=False):
"""Convert empty dicts to None for plan entries so they output as `key:` instead of `key: {}`."""
if isinstance(data, dict):
result = {}
for key, value in data.items():
is_plan_section = in_plan or key in ('fresh_installs', 'upgrades_new', 'upgrades_old', 'old_removes')
if isinstance(value, dict):
if len(value) == 0 and is_plan_section:
result[key] = None
else:
result[key] = convert_empty_dicts_to_none(value, is_plan_section)
elif isinstance(value, list):
result[key] = [convert_empty_dicts_to_none(item, is_plan_section) for item in value]
else:
result[key] = value
return result
elif isinstance(data, list):
return [convert_empty_dicts_to_none(item, in_plan) for item in data]
else:
return data
def parse_installed_file(file_path: Path) -> Dict:
"""Parse an .installed file and return installed packages dict."""
packages = parse_apk_index_file(file_path)
installed = {}
for pkg in packages:
pkgname = pkg.get('pkgname', '')
version = pkg.get('version', '1')
arch = pkg.get('arch', 'x86_64')
pkgkey = f"{pkgname}__{version}__{arch}"
installed[pkgkey] = {
'arch': arch,
'depend_depth': 0,
'install_time': 1000000000,
'ebin_exposure': True,
'rdepends': [],
'depends': [],
'ebin_links': []
}
installed = clean_yaml_data(installed)
return installed
def extract_request_from_args(args: str) -> Tuple[Dict[str, List[str]], Dict]:
"""Extract package requests from @ARGS line.
Returns a tuple of (request_dict, flags_dict) where:
- request_dict has 'install', 'upgrade', 'remove' fields
- flags_dict has 'force' (for --force), 'upgrade_flag' (for --upgrade), 'ignore' (list of ignored packages)
Returns None if the command is 'fix' (should skip this test).
"""
parts = args.split()
request = {
'install': [],
'upgrade': [],
'remove': []
}
flags = {
'force': False,
'upgrade_flag': False,
'ignore': []
}
current_command = None
i = 0
while i < len(parts):
if parts[i] == 'add':
current_command = 'install'
i += 1
elif parts[i] == 'upgrade':
current_command = 'upgrade'
i += 1
elif parts[i] == 'fix':
return None
elif parts[i] == 'del':
current_command = 'remove'
i += 1
elif parts[i] == '--force':
flags['force'] = True
i += 1
elif parts[i] == '--upgrade':
flags['upgrade_flag'] = True
if current_command == 'install':
current_command = 'upgrade'
i += 1
elif parts[i] == '--ignore':
i += 1
while i < len(parts) and not parts[i].startswith('--'):
flags['ignore'].append(parts[i])
i += 1
elif parts[i] == '--no-network':
i += 1
continue
elif parts[i] == '-a':
i += 1
continue
elif parts[i].startswith('--'):
i += 1
continue
elif parts[i].startswith('-') and len(parts[i]) > 1:
i += 1
continue
else:
if current_command:
request[current_command].append(parts[i])
i += 1
return request, flags
def extract_plan_from_expect(expect: List[str]) -> Tuple[Dict, List[str], bool]:
"""Extract InstallationPlan from @EXPECT section."""
plan = {}
missing = []
expect_fail = False
fresh_installs = {}
upgrades_new = {}
upgrades_old = {}
old_removes = {}
for line in expect:
if 'ERROR:' in line or 'breaks:' in line:
expect_fail = True
continue
match = re.search(r'Installing\s+(\S+)\s+\((\S+)\)', line)
if match:
pkgname = match.group(1)
version = match.group(2)
pkgkey = f"{pkgname}__{version}__x86_64"
fresh_installs[pkgkey] = {}
match = re.search(r'Replacing\s+(\S+)\s+\((\S+)\s+->\s+(\S+)\)', line)
if match:
pkgname = match.group(1)
old_version = match.group(2)
new_version = match.group(3)
old_pkgkey = f"{pkgname}__{old_version}__x86_64"
new_pkgkey = f"{pkgname}__{new_version}__x86_64"
upgrades_old[old_pkgkey] = {}
upgrades_new[new_pkgkey] = {}
match = re.search(r'Upgrading\s+(\S+)\s+\((\S+)\s+->\s+(\S+)\)', line)
if match:
pkgname = match.group(1)
old_version = match.group(2)
new_version = match.group(3)
old_pkgkey = f"{pkgname}__{old_version}__x86_64"
new_pkgkey = f"{pkgname}__{new_version}__x86_64"
upgrades_old[old_pkgkey] = {}
upgrades_new[new_pkgkey] = {}
match = re.search(r'Purging\s+(\S+)\s+\((\S+)\)', line)
if match:
pkgname = match.group(1)
version = match.group(2)
pkgkey = f"{pkgname}__{version}__x86_64"
old_removes[pkgkey] = {}
if fresh_installs:
plan['fresh_installs'] = fresh_installs
if upgrades_new:
plan['upgrades_new'] = upgrades_new
if upgrades_old:
plan['upgrades_old'] = upgrades_old
if old_removes:
plan['old_removes'] = old_removes
return plan, missing, expect_fail
def get_test_category(test_name: str) -> str:
"""Determine the category/subdirectory for a test."""
if test_name.startswith('basic'):
return 'basic'
elif test_name.startswith('complicated'):
return 'complicated'
elif test_name.startswith('conflict'):
return 'conflict'
elif test_name.startswith('error'):
return 'error'
elif test_name.startswith('fuzzy'):
return 'fuzzy'
elif test_name.startswith('installif'):
return 'installif'
elif test_name.startswith('pinning'):
return 'pinning'
elif test_name.startswith('provides'):
return 'provides'
elif test_name.startswith('selfupgrade'):
return 'selfupgrade'
elif test_name.startswith('upgrade'):
return 'upgrade'
elif test_name.startswith('fix'):
return 'fix'
else:
return 'misc'
def get_test_number(test_name: str) -> int:
"""Extract test number from test name."""
match = re.search(r'(\d+)$', test_name)
if match:
return int(match.group(1))
return 1
def port_test(test_file: Path):
"""Port a single test file."""
test_name = test_file.stem
category = get_test_category(test_name)
test_num = get_test_number(test_name)
target_dir = TARGET_DIR / category
target_dir.mkdir(parents=True, exist_ok=True)
test_data = parse_test_file(test_file)
repo_files = []
for tag, repo_file in test_data['repo']:
repo_path = SOURCE_DIR / repo_file
if repo_path.exists():
packages = parse_apk_index_file(repo_path)
yaml_packages = [convert_package_to_yaml(pkg) for pkg in packages]
yaml_packages = clean_yaml_data(yaml_packages)
repo_base = Path(repo_file).stem
if repo_base == category or repo_base == f"{category}1":
if tag:
repo_filename = f"repo_{tag}.yaml"
else:
repo_filename = "repo.yaml"
else:
if tag:
repo_filename = f"{repo_base}_{tag}.yaml"
else:
repo_filename = f"{repo_base}.yaml"
repo_target = target_dir / repo_filename
with open(repo_target, 'w') as f:
yaml.dump(yaml_packages, f, default_flow_style=False, sort_keys=False)
repo_files.append(repo_filename)
installed_file = None
if test_data['installed']:
installed_path = SOURCE_DIR / test_data['installed']
if installed_path.exists():
installed = parse_installed_file(installed_path)
installed_filename = "installed.yaml"
installed_target = target_dir / installed_filename
with open(installed_target, 'w') as f:
yaml.dump(installed, f, default_flow_style=False, sort_keys=False)
installed_file = installed_filename
result = extract_request_from_args(test_data['args'])
if result is None:
print(f"Skipping {test_file.name} (contains 'fix' command)")
return
request, flags = result
if test_data['world']:
world_pkgs = test_data['world'].split()
if request['remove']:
world_pkgs = [pkg for pkg in world_pkgs if pkg not in request['remove']]
args_lower = test_data['args'].lower()
args_parts = args_lower.split()
is_upgrade_command = args_parts and args_parts[0] == 'upgrade'
if world_pkgs:
if is_upgrade_command:
request['upgrade'].extend(world_pkgs)
request['upgrade'] = list(dict.fromkeys(request['upgrade']))
else:
request['install'].extend(world_pkgs)
request['install'] = list(dict.fromkeys(request['install']))
if flags.get('ignore') and request['upgrade']:
request['upgrade'] = [pkg for pkg in request['upgrade'] if pkg not in flags['ignore']]
plan, missing, expect_fail = extract_plan_from_expect(test_data['expect'])
test_yaml = {
'format': 'apk',
'description': f"{category} test {test_num}",
'skip': False,
'repo': ' '.join(repo_files),
}
if installed_file:
test_yaml['installed'] = installed_file
if flags['force']:
test_yaml['config'] = {
'ignore_missing': True
}
if request['install']:
test_yaml['install'] = request['install']
if request['upgrade']:
test_yaml['upgrade'] = request['upgrade']
if request['remove']:
test_yaml['remove'] = request['remove']
if plan:
test_yaml['plan'] = plan
if missing:
test_yaml['missing'] = missing
if expect_fail:
test_yaml['expect_fail'] = expect_fail
test_filename = f"test{test_num}.yaml"
test_target = target_dir / test_filename
test_yaml = {k: v for k, v in test_yaml.items() if v not in ([], {}, None, '')}
test_yaml = clean_yaml_data(test_yaml)
test_yaml = convert_empty_dicts_to_none(test_yaml)
yaml_str = yaml.dump(test_yaml, default_flow_style=False, sort_keys=False, allow_unicode=True)
yaml_str = re.sub(r': null$', ':', yaml_str, flags=re.MULTILINE)
with open(test_target, 'w') as f:
f.write(yaml_str)
print(f"Ported {test_file.name} -> {test_target}")
def main():
"""Main function to port all tests."""
test_files = sorted(SOURCE_DIR.glob("*.test"))
for test_file in test_files:
try:
port_test(test_file)
except Exception as e:
print(f"Error porting {test_file.name}: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()