Files
2026-10-15 20:44:05 -03:00

130 lines
7.2 KiB
Python
Executable File

#!/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))