Files
MeanField_SERiF/tools/test_mfem_build_policy.py

212 lines
9.7 KiB
Python

"""MFEM build-policy regressions; no MFEM sources or downloaded dependencies needed.
Run with ``python tools/test_mfem_build_policy.py``. The integration test compiles
a tiny C/C++ fixture with local CMake; the cache test never launches a compiler.
"""
import contextlib
import io
import json
import os
from pathlib import Path
import platform
import shlex
import shutil
import subprocess
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
import build_mfem_bundle as bundle
FEATURES = dict.fromkeys(bundle.CMAKE_FEATURES, False)
CMAKE = shutil.which("cmake")
CC = shutil.which("cc")
CXX = shutil.which("c++")
def arguments(root, mode="release", optimization="2", debug=False, ndebug=False):
return [
"build_mfem_bundle.py",
"--source", str(root / "source"),
"--work-dir", str(root / "bundle" / "work"),
"--prefix", str(root / "prefix"),
"--cmake", CMAKE or "cmake",
"--cc", CC or "cc", "--cxx", CXX or "c++",
"--buildtype", mode, "--optimization", optimization,
"--debug", str(debug).lower(), "--ndebug", str(ndebug).lower(),
"--compiler-syntax", "gcc", "--compiler-id", "clang",
"--jobs", "1", "--host-system", platform.system().lower(),
"--cross-build", "false", "--library-kind", "static",
"--allow-system", "true", "--vendor-dependency-prefix", "false",
*[item for name in FEATURES for item in ("--feature", name + "=OFF")],
]
def parse(root, **kwargs):
with patch.object(sys, "argv", arguments(root, **kwargs)):
return bundle.parse_args()
def public_header(prefix, debug=False):
header = prefix / "include" / "mfem" / "config" / "_config.hpp"
header.parent.mkdir(parents=True, exist_ok=True)
header.write_text("#define MFEM_USE_DOUBLE\n" +
("#define MFEM_DEBUG\n" if debug else ""))
return header
class MfemBuildPolicyTests(unittest.TestCase):
def test_assertions_symbols_and_optimization_are_independent(self):
args = SimpleNamespace(compiler_syntax="gcc", compiler_id="clang",
optimization="2", debug="false", ndebug="false")
self.assertEqual(shlex.split(bundle.mfem_compile_flags(args)), ["-O2"])
args.debug = "true"
self.assertNotIn("-DNDEBUG", shlex.split(bundle.mfem_compile_flags(args)))
self.assertIn("-g", shlex.split(bundle.mfem_compile_flags(args)))
args.optimization = "s"
self.assertIn("-Oz", shlex.split(bundle.mfem_compile_flags(args)))
args.compiler_id = "gcc"
self.assertIn("-Os", shlex.split(bundle.mfem_compile_flags(args)))
args.optimization = "plain"
args.debug = "false"
self.assertEqual(bundle.mfem_compile_flags(args), "")
args.compiler_syntax = "msvc"
args.optimization = "2"
args.ndebug = "true"
self.assertEqual(shlex.split(bundle.mfem_compile_flags(args)),
["/O2", "/DNDEBUG"])
def test_public_header_checks_mfem_debug_in_both_directions(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for mode, debug in [("debug", True), ("release", False),
("debugoptimized", False)]:
with self.subTest(mode=mode):
args = parse(root, mode=mode)
public_header(args.prefix, debug)
bundle.validate_mfem_feature_header(args, FEATURES)
public_header(args.prefix, not debug)
with self.assertRaisesRegex(RuntimeError, "MFEM_DEBUG"):
bundle.validate_mfem_feature_header(args, FEATURES)
def test_cache_reconfiguration_and_cache_hit_header_validation(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "source"
source.mkdir()
(source / "CMakeLists.txt").write_text("# Stable source fingerprint\n")
def fake_install(args, features, env, build_type, mpicc, mpicxx):
public_header(args.prefix, build_type == "Debug")
(args.prefix / "include" / "mfem.hpp").write_text("// fixture\n")
(args.prefix / "lib").mkdir(parents=True, exist_ok=True)
(args.prefix / "lib" / "libmfem.a").write_bytes(b"fixture archive")
with patch.object(bundle, "output", return_value="fixture compiler"), \
patch.object(bundle, "build_mfem", side_effect=fake_install) as build, \
patch.object(bundle, "install_licenses"), \
patch.object(bundle, "sanitize_installed_metadata"), \
contextlib.redirect_stdout(io.StringIO()):
def invoke(**kwargs):
with patch.object(sys, "argv", arguments(root, **kwargs)):
return bundle.main()
self.assertEqual(invoke(mode="debug", optimization="0", debug=True), 0)
self.assertEqual(invoke(), 0) # Same directory; release invalidates debug.
self.assertEqual(build.call_count, 2)
self.assertEqual(invoke(), 0) # Unchanged valid prefix is reusable.
self.assertEqual(build.call_count, 2)
header = public_header(root / "prefix", debug=True)
with self.assertRaisesRegex(RuntimeError, "MFEM_DEBUG"):
invoke() # Same fingerprint must not hide an incompatible header.
self.assertEqual(build.call_count, 2)
header.unlink()
with self.assertRaisesRegex(RuntimeError, "_config.hpp.*missing"):
invoke()
@unittest.skipUnless(CMAKE and CC and CXX and os.name != "nt",
"requires local CMake and GCC-compatible C/C++ compilers")
def test_real_cmake_reconfigure_replaces_implicit_flags_for_both_languages(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "source"
source.mkdir()
(source / "CMakeLists.txt").write_text('''\
cmake_minimum_required(VERSION 3.18)
project(mfem_policy_fixture LANGUAGES C CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Match the upstream MFEM build-type rule, including debugoptimized behavior.
if(CMAKE_BUILD_TYPE MATCHES "Debug|debug|DEBUG")
set(MFEM_DEBUG ON)
else()
set(MFEM_DEBUG OFF)
endif()
configure_file(config.hpp.in mfem_config.hpp)
add_executable(policy_probe probe.cpp probe.c)
target_include_directories(policy_probe PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}")
install(TARGETS policy_probe DESTINATION bin)
''')
(source / "config.hpp.in").write_text("#cmakedefine MFEM_DEBUG\n")
checks = '''\
#include "expected.hpp"
#include "mfem_config.hpp"
#if defined(NDEBUG) != EXPECT_NDEBUG
#error CMake changed the requested assertion policy
#endif
#if defined(MFEM_DEBUG) != EXPECT_MFEM_DEBUG
#error Stale MFEM debug configuration
#endif
#if defined(__OPTIMIZE__) != EXPECT_OPTIMIZED
#error Requested optimization was not applied
#endif
'''
(source / "probe.c").write_text(checks + "int c_probe(void) { return 17; }\n")
(source / "probe.cpp").write_text(checks + '''\
extern "C" int c_probe(void);
int main() { return c_probe() == 17 ? 0 : 1; }
''')
def quiet_run(command, *, cwd=None, env=None):
result = subprocess.run([os.fspath(x) for x in command], cwd=cwd,
env=env, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, timeout=20)
self.assertEqual(result.returncode, 0, result.stdout)
cases = [("debug", "0", True, False),
("release", "3", False, False),
("release", "2", False, False),
("release", "2", False, True),
("debugoptimized", "2", True, False),
("debug", "0", True, False)]
with patch.object(bundle, "prepare_mfem_source", return_value=source), \
patch.object(bundle, "run", side_effect=quiet_run):
for mode, optimization, debug, ndebug in cases:
with self.subTest(mode=mode, ndebug=ndebug):
args = parse(root, mode=mode, optimization=optimization,
debug=debug, ndebug=ndebug)
(source / "expected.hpp").write_text(
f"#define EXPECT_NDEBUG {int(ndebug)}\n"
f"#define EXPECT_MFEM_DEBUG {int(mode == 'debug')}\n"
f"#define EXPECT_OPTIMIZED {int(optimization != '0')}\n")
bundle.build_mfem(args, FEATURES, os.environ.copy(),
bundle.cmake_build_type(mode), "", "")
build = args.work_dir / "mfem"
commands = json.loads((build / "compile_commands.json").read_text())
self.assertEqual({Path(x["file"]).suffix for x in commands}, {".c", ".cpp"})
for command in commands:
tokens = shlex.split(command["command"])
self.assertEqual([token for token in tokens
if token.startswith("-O")],
["-O" + optimization])
self.assertEqual("-DNDEBUG" in tokens, ndebug)
self.assertEqual("-g" in tokens, debug)
quiet_run([args.prefix / "bin" / "policy_probe"])
if __name__ == "__main__":
unittest.main()