3 changed files with 213 additions and 2 deletions
@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3 |
||||
"""Проверка настоящего build.sh на маленьком проекте Autotools, без изменения исходников uTun.""" |
||||
from pathlib import Path |
||||
import json |
||||
import os |
||||
import shutil |
||||
import subprocess |
||||
import sys |
||||
import tempfile |
||||
import time |
||||
|
||||
script = Path(sys.argv[1]).resolve() |
||||
base = Path(tempfile.mkdtemp(prefix='utun-build-tests-')) |
||||
log = base / 'commands.log' |
||||
environment = {**os.environ, 'ASAN_OPTIONS': 'detect_leaks=0'} |
||||
environment.pop('MAKEFLAGS', None) |
||||
environment.pop('MSYSTEM', None) |
||||
for name in ['CC', 'CFLAGS', 'CPPFLAGS', 'LDFLAGS', 'LIBS', 'DEBUG_FLAGS']: |
||||
environment.pop(name, None) |
||||
checks = [] |
||||
|
||||
def write(name, content): |
||||
path = base / name |
||||
path.parent.mkdir(parents=True, exist_ok=True) |
||||
path.write_text(content) |
||||
return path |
||||
|
||||
def run(args, ok=True, extra_environment=None): |
||||
result = subprocess.run(args, cwd=base, env={**environment, **(extra_environment or {})}, text=True, capture_output=True) |
||||
with log.open('a') as output: |
||||
output.write(f'{args!r}\nexit={result.returncode}\n{result.stdout}{result.stderr}\n') |
||||
if ok and result.returncode: |
||||
raise AssertionError(f'{args!r}: exit={result.returncode}; see {log}\n{result.stderr[-1500:]}') |
||||
return result |
||||
|
||||
def build(*args, **kwargs): |
||||
return run(['bash', 'build.sh', '--no-gui', '-j', '2', *args], **kwargs) |
||||
|
||||
def check(name, condition): |
||||
if not condition: |
||||
raise AssertionError(f'{name}; see {log}') |
||||
checks.append(name) |
||||
print(f'[PASS] build.sh: {name}', flush=True) |
||||
|
||||
def times(): |
||||
return {str(p.relative_to(base)): p.stat().st_mtime_ns for p in base.rglob('*.o')} |
||||
|
||||
def value(): |
||||
return run(['./src/utun']).stdout.strip() |
||||
|
||||
try: |
||||
shutil.copyfile(script, base / 'build.sh') |
||||
write('configure.ac', '''AC_INIT([build-test], [1.0]) |
||||
AC_CONFIG_SRCDIR([src/main.c]) |
||||
AC_CONFIG_HEADERS([config.h]) |
||||
AC_CONFIG_MACRO_DIR([m4]) |
||||
AM_INIT_AUTOMAKE([foreign subdir-objects]) |
||||
AC_PROG_CC |
||||
AC_PROG_RANLIB |
||||
AM_PROG_AR |
||||
AC_ARG_WITH([ffmpeg], [], [], [with_ffmpeg=no]) |
||||
AC_ARG_ENABLE([hardening], [], [], [enable_hardening=no]) |
||||
if test "x$enable_hardening" = xyes; then |
||||
CFLAGS="$CFLAGS -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE" |
||||
LDFLAGS="$LDFLAGS -Wl,-z,relro -Wl,-z,now -pie" |
||||
fi |
||||
TEST_CONFIGURE_OPTION |
||||
AC_CONFIG_FILES([Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile]) |
||||
AC_OUTPUT |
||||
''') |
||||
write('m4/test.m4', 'AC_DEFUN([TEST_CONFIGURE_OPTION], [AC_DEFINE([TEST_OPTION], [1], [Test option])])\n') |
||||
write('Makefile.am', 'SUBDIRS = lib lib/libopus src tests\n') |
||||
write('lib/Makefile.am', 'noinst_LIBRARIES = libfixture.a\nlibfixture_a_SOURCES = fixture.c\n') |
||||
write('lib/fixture.c', 'int fixture(void) { return 0; }\n') |
||||
write('lib/libopus/Makefile.am', '''noinst_LIBRARIES = libopus_internal.a |
||||
libopus_internal_a_SOURCES = opus.c |
||||
CFLAGS = -g -Og -fno-omit-frame-pointer -fno-pie -fPIC |
||||
''') |
||||
write('lib/libopus/opus.c', 'int opus_fixture(void) { return 0; }\n') |
||||
write('src/Makefile.am', '''bin_PROGRAMS = utun |
||||
utun_SOURCES = main.c worker.c api.h nested.h |
||||
AM_CFLAGS = -DLOCAL_OPTION=1 |
||||
utun_LDADD = ../lib/libfixture.a ../lib/libopus/libopus_internal.a |
||||
''') |
||||
write('src/main.c', '''#include <stdio.h> |
||||
#include "api.h" |
||||
int worker(void); |
||||
int fixture(void); |
||||
int opus_fixture(void); |
||||
int main(void) { printf("%d\\n", worker() + fixture() + opus_fixture()); return 0; } |
||||
''') |
||||
write('src/worker.c', '#include "api.h"\nint worker(void) { return VALUE + LOCAL_OPTION; }\n') |
||||
write('src/api.h', '#include "nested.h"\n') |
||||
write('src/nested.h', '#define VALUE 10\n') |
||||
write('tests/Makefile.am', 'noinst_PROGRAMS = probe\nprobe_SOURCES = probe.c\n') |
||||
write('tests/probe.c', 'int main(void) { return 0; }\n') |
||||
package_tool = write('bin/pkg-config', '''#!/bin/bash |
||||
[[ "${BUILD_TEST_FFMPEG:-0}" = 1 ]] || exit 1 |
||||
case "$1" in |
||||
--exists) exit 0 ;; |
||||
--modversion) echo 1.0 ;; |
||||
--cflags|--libs-only-L) exit 0 ;; |
||||
*) exit 1 ;; |
||||
esac |
||||
''') |
||||
package_tool.chmod(0o755) |
||||
environment['PATH'] = str(base / 'bin') + os.pathsep + environment['PATH'] |
||||
|
||||
result = build() |
||||
check('fresh build generates, configures and builds dependencies', value() == '11' and (base / 'lib/libopus/libopus_internal.a').exists()) |
||||
check('normal build includes tests', (base / 'tests/probe').exists()) |
||||
initial = times() |
||||
result = build() |
||||
check('repeat build does not regenerate, configure or compile', initial == times() and 'INFO: configuring:' not in result.stdout and 'INFO: regenerating' not in result.stdout) |
||||
time.sleep(1.1) |
||||
write('src/nested.h', '#define VALUE 20\n') |
||||
result = build() |
||||
current = times() |
||||
check('transitive header rebuilds both dependent objects', current['src/main.o'] > initial['src/main.o'] and current['src/worker.o'] > initial['src/worker.o']) |
||||
check('header change preserves unrelated objects and configuration', current['lib/fixture.o'] == initial['lib/fixture.o'] and 'INFO: configuring:' not in result.stdout and value() == '21') |
||||
initial = times() |
||||
write('src/unused.h', '#define UNUSED 1\n') |
||||
build() |
||||
check('unrelated header does not recompile', initial == times()) |
||||
|
||||
for mode in ['asan', 'hardened', 'normal']: |
||||
args = [] if mode == 'normal' else ['--' + mode] |
||||
result = build(*args) |
||||
symbols = run(['nm', '-u', 'src/worker.o']).stdout |
||||
check(f'{mode}: changing mode reconfigures and cleans', 'INFO: configuring:' in result.stdout and 'INFO: cleaning objects:' in result.stdout) |
||||
check(f'{mode}: object instrumentation matches mode', ('__asan_' in symbols) == (mode == 'asan')) |
||||
if mode == 'asan': |
||||
check('ASAN skips tests and preserves unsanitized Opus', not (base / 'tests/probe').exists() and '__asan_' not in run(['nm', '-u', 'lib/libopus/opus.o']).stdout) |
||||
initial = times() |
||||
result = build(*args) |
||||
check(f'{mode}: repeat build is incremental', initial == times() and 'INFO: configuring:' not in result.stdout and 'INFO: regenerating' not in result.stdout) |
||||
|
||||
result = build('CFLAGS=-g -O0 -DUSER_OPTION=1') |
||||
check('quoted compiler flags are passed intact through configure', 'CFLAGS = -g -O0 -DUSER_OPTION=1' in (base / 'src/Makefile').read_text()) |
||||
initial = times() |
||||
build('CFLAGS=-g -O0 -DUSER_OPTION=1') |
||||
check('repeat build with quoted flags is incremental', initial == times()) |
||||
build() |
||||
time.sleep(1.1) |
||||
write('src/Makefile.am', (base / 'src/Makefile.am').read_text().replace('LOCAL_OPTION=1', 'LOCAL_OPTION=2')) |
||||
result = build() |
||||
check('Makefile.am flag change rebuilds stale objects', 'INFO: regenerating' in result.stdout and value() == '22') |
||||
macro = base / 'm4/test.m4' |
||||
saved_time = macro.stat() |
||||
write('m4/test.m4', macro.read_text().replace('[1]', '[2]')) |
||||
os.utime(macro, ns=(saved_time.st_atime_ns, saved_time.st_mtime_ns)) |
||||
build() |
||||
check('macro content change is detected with unchanged timestamp', '#define TEST_OPTION 2' in (base / 'config.h').read_text()) |
||||
result = build(extra_environment={'BUILD_TEST_FFMPEG': '1'}) |
||||
check('newly available optional dependency reconfigures', 'INFO: configuring:' in result.stdout and '--with-ffmpeg' in run(['./config.status', '--config']).stdout) |
||||
result = build() |
||||
check('disappearing optional dependency reconfigures', 'INFO: configuring:' in result.stdout and '--without-ffmpeg' in run(['./config.status', '--config']).stdout) |
||||
(base / 'src/.deps/worker.Po').unlink() |
||||
result = build() |
||||
check('missing dependencies force rebuild without configure', 'missing_dependencies=1' in result.stdout and 'INFO: configuring:' not in result.stdout) |
||||
write('src/.deps/worker.Po', '# dummy\n') |
||||
result = build() |
||||
check('dummy dependencies force rebuild', 'missing_dependencies=1' in result.stdout) |
||||
result = build('--clean') |
||||
check('explicit clean does not reconfigure', 'INFO: cleaning objects:' in result.stdout and 'INFO: configuring:' not in result.stdout) |
||||
result = build('--full') |
||||
check('explicit full regenerates, configures and rebuilds', 'INFO: regenerating' in result.stdout and 'INFO: configuring:' in result.stdout) |
||||
(base / 'src/Makefile').unlink() |
||||
result = build() |
||||
check('missing child Makefile is recreated', (base / 'src/Makefile').exists() and 'INFO: configuring:' in result.stdout) |
||||
run(['./configure', 'CFLAGS=-O0']) |
||||
result = build() |
||||
check('external configure is detected', 'INFO: configuring:' in result.stdout and 'CFLAGS = -g -O2' in (base / 'src/Makefile').read_text()) |
||||
check('conflicting modes fail', build('--asan', '--hardened', ok=False).returncode != 0) |
||||
check('invalid job count fails', build('-j0', ok=False).returncode != 0) |
||||
|
||||
state = (base / '.utun-build-state').read_bytes() |
||||
original = (base / 'src/worker.c').read_text() |
||||
write('src/worker.c', original + '\n#error deliberate_failure\n') |
||||
check('compiler failure propagates without committing state', build(ok=False).returncode != 0 and (base / '.utun-build-state').read_bytes() == state) |
||||
write('src/worker.c', original) |
||||
check('build recovers from compiler failure', build().returncode == 0 and value() == '22') |
||||
original = (base / 'configure.ac').read_text() |
||||
write('configure.ac', original + '\nm4_fatal([deliberate_autoconf_failure])\n') |
||||
check('autoreconf failure propagates through tee', build(ok=False).returncode != 0 and (base / '.utun-build-state').read_bytes() == state) |
||||
write('configure.ac', original) |
||||
build() |
||||
write('tools/chatgui/build/CMakeCache.txt', 'placeholder\n') |
||||
cmake = write('bin/cmake', '#!/bin/bash\nprintf "%s\\n" "$*" > gui-arguments\nexit 9\n') |
||||
cmake.chmod(0o755) |
||||
result = run(['bash', 'build.sh', '-j', '3'], ok=False) |
||||
check('GUI failure propagates and success is not printed', result.returncode != 0 and 'Build completed successfully!' not in result.stdout) |
||||
check('GUI receives selected job count', (base / 'gui-arguments').read_text().strip() == '--build tools/chatgui/build --parallel 3') |
||||
print(f'[PASS] build.sh: {len(checks)} checks completed', flush=True) |
||||
shutil.rmtree(base) |
||||
except Exception: |
||||
print(f'[FAIL] build.sh: diagnostic files retained in {base}', file=sys.stderr) |
||||
raise |
||||
Loading…
Reference in new issue