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