212 lines
9.7 KiB
Python
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()
|