#!/usr/bin/python3 """Integration checks in the built image; graphical checks are recorded separately.""" import configparser import json import os from pathlib import Path import shutil import subprocess import tempfile import xml.etree.ElementTree as ET passed = failed = 0 def check(name, fn): global passed, failed try: fn() print('PASS ', name, flush=True) passed += 1 except Exception as error: print('FAIL ', name, ':', error, flush=True) failed += 1 def require(condition, detail='assertion failed'): if not condition: raise AssertionError(detail) def run(args, **kwargs): result = subprocess.run(args, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=45, **kwargs) require(result.returncode == 0, result.stdout) return result.stdout samples = Path(__file__).parent / 'ide_samples' with tempfile.TemporaryDirectory(prefix='ACPC IDE check ') as directory: work = Path(directory) for filename in ('sum.c', 'sum.cpp', 'Sum.java', 'sum.py'): case = work / filename.replace('.', '-') case.mkdir() source = case / filename shutil.copy(samples / filename, source) def exercise(source=source): output = run(['acpc-solution', 'run', str(source)], input='3000000000 4\n') require('SUM = 3000000004' in output, output) if source.suffix in ('.c', '.cpp'): require('statically linked' in run(['file', str(source.with_suffix(''))])) if source.suffix == '.java': require(int.from_bytes(source.with_suffix('.class').read_bytes()[6:8], 'big') == 69) print(' ' + output.strip().replace('\n', '\n ')) check('build and execute ' + filename + ' (path contains spaces)', exercise) def driver_flags(): log = work / 'compiler.jsonl' for lang, driver, old_standard, expected, library in ( ('c', 'acpc-gcc', 'c99', 'gnu23', '-lm'), ('cpp', 'acpc-g++', 'c++17', 'gnu++23', '-lpthread'), ): source = work / ('sum-' + lang) / ('sum.' + lang) run([driver, '-O0', '-std=' + old_standard, str(source), '-o', str(work / driver)], env={**os.environ, 'ACPC_COMPILER_LOG': str(log)}) args = json.loads(log.read_text().splitlines()[-1])['argv'] require('-O0' not in args and '-O2' in args and '-std=' + expected in args, str(args)) require('-static' in args and library in args, str(args)) require('15.2.0' in run(['acpc-gcc', '-dumpfullversion'])) require('15.2.0' in run(['acpc-g++', '--version'])) output = run(['acpc-cc', '-x', 'c++', '-dM', '-E', '-'], input='') require('#define __cplusplus 202302L' in output, 'C++ compiler probes must use the C++ backend') response = work / 'compiler probe.args' response.write_text('-x c++ -std=c++17 -dM -E -') output = run(['acpc-cc', '@' + str(response)], input='') require('#define __cplusplus 202302L' in output, 'CLion response-file probes must see the same C++23 standard as compilation') check('compiler drivers override generic project flags, link statically and answer IDE probes', driver_flags) def failed_build(): case = work / 'sum-cpp' source = case / 'sum.cpp' source.write_text('#error deliberate bad source\n') result = subprocess.run(['acpc-solution', 'run', str(source)], text=True, input='1 2\n', capture_output=True) require(result.returncode != 0 and 'SUM =' not in result.stdout and 'Run:' not in result.stdout, 'a failed build must never execute the previous binary') check('failed build does not execute a stale binary', failed_build) def geany_commands(): for language, filename in [('c', 'sum.c'), ('cpp', 'sum.cpp'), ('java', 'Sum.java'), ('python', 'sum.py')]: conf = configparser.ConfigParser(interpolation=None, strict=False) conf.read('/usr/share/geany/filedefs/filetypes.' + language) source = work / ('geany ' + language) / filename source.parent.mkdir() shutil.copy(samples / filename, source) for key in ('FT_00_CM', 'FT_01_CM', 'EX_00_CM'): command = conf['build-menu'][key].replace('%f', source.name).replace('%e', source.stem) result = run(['bash', '-c', command], cwd=source.parent, input='3000000000 4\n') if key == 'EX_00_CM': require('SUM = 3000000004' in result, result) if key == 'FT_00_CM' and source.suffix in ('.c', '.cpp'): require('relocatable' in run(['file', str(source.with_suffix('.o'))])) check('all four installed Geany build and execute commands work', geany_commands) for home in (Path('/home/team'), Path('/etc/skel')): def profiles(home=home): cb = ET.parse(home / '.config/codeblocks/default.conf') cb_root = Path(cb.findtext('./compiler/sets/gcc/MASTER_PATH/str')) / 'bin' require((cb_root / cb.findtext('./compiler/sets/gcc/C_COMPILER/str')).resolve() == Path('/usr/local/bin/acpc-cc')) require((cb_root / cb.findtext('./compiler/sets/gcc/CPP_COMPILER/str')).resolve() == Path('/usr/local/bin/acpc-cc')) require(not Path(cb.findtext('./compiler/sets/gcc/C_COMPILER/str')).is_absolute(), 'Code::Blocks prefixes MASTER_PATH/bin, including for an absolute filename') code = home / '.config/Code/User' tasks = json.loads((code / 'tasks.json').read_text())['tasks'] require(any(t['label'] == 'ACPC: Build and run' and t['command'] == '/usr/local/bin/acpc-solution' for t in tasks)) require(any(b.get('key') == 'f6' and b.get('args') == 'ACPC: Build and run' for b in json.loads((code / 'keybindings.json').read_text()))) for product in ('CLion', 'PyCharm'): options = next((home / '.config/JetBrains').glob(product + '*/options')) require('/usr/local/bin/pypy3' in (options / 'jdk.table.xml').read_text()) options = next((home / '.config/JetBrains').glob('IntelliJIdea*/options')) require('ACPC Java 25' in (options / 'jdk.table.xml').read_text()) require('-XX:+UseSerialGC' in (options / 'project.default.xml').read_text()) options = next((home / '.config/JetBrains').glob('CLion*/options')) chain = ET.parse(options / 'linux/toolchains.xml').find('./component/toolchains/toolchain') require(chain.get('customCCompilerPath') == '/usr/local/bin/acpc-gcc') require(chain.get('customCXXCompilerPath') == '/usr/local/bin/acpc-g++') workspace = home / 'eclipse-workspace/.metadata/.plugins' launch = ET.parse(workspace / 'org.eclipse.debug.core/.launches/ACPC Build and run.launch') require(any(n.get('value') == '/usr/local/bin/acpc-solution' for n in launch.getroot())) prefs = workspace / 'org.eclipse.core.runtime/.settings' require('/usr/local/bin/pypy3' in (prefs / 'org.python.pydev.prefs').read_text()) require('java-25-openjdk' in (prefs / 'org.eclipse.jdt.launching.prefs').read_text()) check(str(home) + ': settings for every IDE are present and parseable', profiles) print(f'\n{passed} passed, {failed} failed') raise SystemExit(bool(failed))