Verify build mode transitions and propagate GUI job counts
This commit is contained in:
1 parent
ff9d205d5b
commit
ff2eae2efb
3 files changed
+213
-2
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user