Browse Source

Verify build mode transitions and propagate GUI job counts

master
evgeny 12 hours ago
parent
commit
ff2eae2efb
  1. 2
      .gitignore
  2. 198
      tests/test_build_script.py
  3. 15
      tools/chatgui/build.sh

2
.gitignore vendored

@ -32,6 +32,7 @@ index.html
*.exe
# Autotools
/.utun-build-state*
Makefile
!tools/etcpmon/Makefile
Makefile.in
@ -84,6 +85,7 @@ logs/
tests/test_*
!tests/test_*.c
!tests/test_*.h
!tests/test_*.py
!tests/test_*.conf
!tests/test_*.json
tests/bench_*

198
tests/test_build_script.py

@ -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

15
tools/chatgui/build.sh

@ -14,6 +14,18 @@ log ""
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$SRC_DIR/build"
RUNTIME_PREFIX="${MSYSTEM_PREFIX:-/ucrt64}"
JOBS=$(nproc 2>/dev/null || echo 4)
while [[ $# -gt 0 ]]; do
case "$1" in
-j)
[[ $# -gt 1 ]] || { log "ERROR: -j requires a positive job count"; exit 1; }
shift; JOBS=$1 ;;
-j*) JOBS=${1#-j} ;;
*) log "ERROR: unknown argument: $1"; exit 1 ;;
esac
shift
done
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { log "ERROR: invalid job count: $JOBS"; exit 1; }
log "Source: $SRC_DIR"
log "Build: $BUILD_DIR"
@ -36,7 +48,7 @@ if [ "${MSYSTEM:-}" = UCRT64 ]; then
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON 2>&1 | tee -a "$LOG_FILE" || {
log " ERROR: bundled rlottie configure failed"; exit 1;
}
cmake --build "$SRC_DIR/rlottie_build" --target rlottie --parallel 4 2>&1 | tee -a "$LOG_FILE" || {
cmake --build "$SRC_DIR/rlottie_build" --target rlottie --parallel "$JOBS" 2>&1 | tee -a "$LOG_FILE" || {
log " ERROR: bundled rlottie build failed"; exit 1;
}
log " OK: bundled rlottie ready in $SRC_DIR/rlottie_build"
@ -47,7 +59,6 @@ cmake -S "$SRC_DIR" -B "$BUILD_DIR" -G Ninja 2>&1 | tee -a "$LOG_FILE"
test ${PIPESTATUS[0]} -eq 0 || { log "CMake configure FAILED"; exit 1; }
log ""
JOBS=$(nproc 2>/dev/null || echo 4)
log "[2/4] Building (parallel: $JOBS jobs)..."
cmake --build "$BUILD_DIR" -j"$JOBS" 2>&1 | tee -a "$LOG_FILE"
test ${PIPESTATUS[0]} -eq 0 || { log "Build FAILED"; exit 1; }

Loading…
Cancel
Save