ZaStoGram_desktop/.agents/shared/evidence/ninja_target_test.py
John Preston 69f14db382 [ai] Add a Ninja target provenance helper for evidence
Task: 2026/09/30/qualify-consuming-ninja-target-provenance
2026-10-08 22:24:47 +04:00

1314 lines
46 KiB
Python

#!/usr/bin/env python3
import json
import os
import re
import shlex
import shutil
import subprocess
import tempfile
from types import SimpleNamespace
import unittest
import ninja_target
NINJA = shutil.which("ninja")
SKIP_CMAKE_VARIABLE = "NINJA_TARGET_TEST_SKIP_CMAKE"
APP_MAIN = "CMakeFiles/app.dir/Debug/app/main.cpp.o"
APP_COMMON = "CMakeFiles/app.dir/Debug/shared/common.cpp.o"
APP_PLAIN = "CMakeFiles/app.dir/Debug/app/plain.c.o"
LIB_UTIL = "lib dir/CMakeFiles/objlib.dir/Debug/util.cpp.o"
LIB_COMMON = "lib dir/CMakeFiles/objlib.dir/Debug/__/shared/common.cpp.o"
TOOL_COMMON = "CMakeFiles/tool.dir/Debug/shared/common.cpp.o"
APP_HEADER = "CMakeFiles/app.dir/Debug/cmake_pch.hxx"
LIB_HEADER = "lib dir/CMakeFiles/objlib.dir/Debug/cmake_pch.hxx"
APP_RULE = "CXX_COMPILER__app_unscanned_Debug"
APP_C_RULE = "C_COMPILER__app_unscanned_Debug"
LIB_RULE = "CXX_COMPILER__objlib_unscanned_Debug"
TOOL_RULE = "CXX_COMPILER__tool_unscanned_Debug"
APP_LINK_RULE = "CXX_EXECUTABLE_LINKER__app_Debug"
TOOL_LINK_RULE = "CXX_EXECUTABLE_LINKER__tool_Debug"
APP_DEFINES = '-DCMAKE_INTDIR=\\"Debug\\" -DFX_APP_POLICY -DFX_LEVEL=0'
APP_OPTIONS = "-UFX_LEVEL -DFX_LEVEL=2"
LIB_DEFINES = '-DCMAKE_INTDIR=\\"Debug\\" -DFX_OBJLIB_POLICY'
TOOL_DEFINES = '-DCMAKE_INTDIR=\\"Debug\\" -DFX_TOOL_POLICY'
CONTAINER_DIR = "/usr/src/fx/out"
def ninja_path(path):
return path.replace("$", "$$").replace(" ", "$ ").replace(":", "$:")
def ninja_value(value):
return value.replace("$", "$$")
def pure_build(configured="/work/out", host=None):
return ninja_target.Build(
build_dir=configured,
manifest="build-Debug.ninja",
config="Debug",
generator="Ninja Multi-Config",
configured=configured,
cache={},
host=host or configured)
def compile_entry(command, output="o.o", file="/src/a.cpp"):
return ninja_target.CompdbEntry("/work/out", command, file, output)
def expectation_options(**values):
defaults = dict(
expect_defined=[],
expect_undefined=[],
option_macro=[],
policy_target="app")
return SimpleNamespace(**{**defaults, **values})
def refuse_runner(argv):
raise AssertionError(f"runner must not be called: {argv}")
class HandTree:
"""A handwritten CMake-shaped Ninja Multi-Config tree; never built."""
def __init__(self, root, dialect="clang", configured=None, rsp=False):
self.root = root
self.build_dir = os.path.join(root, "build tree")
os.makedirs(os.path.join(self.build_dir, "CMakeFiles"))
self.source_dir = os.path.join(root, "src dir")
self.configured = configured or self.build_dir
self.dialect = dialect
self.rsp = rsp
self.pch_suffix = ".pch" if dialect == "clang" else ".gch"
self.prebuilt = os.path.join(root, "pre built", "init.cpp.o")
self.cache = [
"# This is the CMakeCache file.",
"",
"//Generator the fixture pretends to be configured with.",
"CMAKE_GENERATOR:INTERNAL=Ninja Multi-Config",
"CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release",
f"CMAKE_CACHEFILE_DIR:INTERNAL={self.configured}",
"FX_DISABLE_FEATURE:BOOL=OFF",
]
self.rules = {}
self.edges = {}
self.populate()
def absolute(self, relative):
return f"{self.configured}/{relative}"
def source(self, relative):
return os.path.join(self.source_dir, relative)
def pch(self, header):
return header + self.pch_suffix
def use_flags(self, header):
quoted = shlex.quote(self.absolute(header))
if self.dialect == "gcc":
return f"-Winvalid-pch -include {quoted}"
pch = shlex.quote(self.absolute(self.pch(header)))
return (f"-Winvalid-pch -Xclang -include-pch -Xclang {pch} "
f"-Xclang -include -Xclang {quoted}")
def emit_flags(self, header):
quoted = shlex.quote(self.absolute(header))
if self.dialect == "gcc":
return f"-x c++-header -include {quoted}"
return f"-Xclang -emit-pch -Xclang -include -Xclang {quoted} -x c++-header"
def add_object(self, output, rule, source, defines, options="", header=None):
flags = " ".join(
part
for part in (options, self.use_flags(header) if header else "")
if part)
self.edges[output] = {
"rule": rule,
"explicit": [source],
"implicit": [header, self.pch(header)] if header else [],
"order_only": [],
"vars": {"DEFINES": defines, "FLAGS": flags},
}
def add_pch(self, header, rule, defines, options=""):
target_dir = os.path.dirname(os.path.dirname(header))
self.edges[self.pch(header)] = {
"rule": rule,
"explicit": [self.absolute(f"{target_dir}/cmake_pch.hxx.cxx")],
"implicit": [header],
"order_only": [],
"vars": {
"DEFINES": defines,
"FLAGS": " ".join(
part for part in (options, self.emit_flags(header)) if part),
},
}
def populate(self):
gcc = self.dialect == "gcc"
compiler = "ccache /usr/bin/g++-13" if gcc else "/usr/bin/c++"
c_compiler = "/usr/bin/gcc" if gcc else "/usr/bin/cc"
linker = "/usr/bin/g++-13" if gcc else "/usr/bin/c++"
tail = "-MD -MT $out -MF $out.d"
for rule in (APP_RULE, LIB_RULE, TOOL_RULE):
self.rules[rule] = {
"command": f"{compiler} $DEFINES $FLAGS {tail} -o $out -c $in",
}
self.rules[APP_C_RULE] = {
"command": f"{c_compiler} $DEFINES $FLAGS {tail} -o $out -c $in",
}
self.rules["CXX_COMPILER__app_wrongout_Debug"] = {
"command": f"{compiler} $DEFINES $FLAGS -o $out.tmp -c $in",
}
self.rules["CXX_COMPILER__app_wrongsrc_Debug"] = {
"command": f"{compiler} $DEFINES $FLAGS -o $out -c $in.cc",
}
self.rules["CUSTOM_COMMAND"] = {"command": "$COMMAND"}
if self.rsp:
link = f"{linker} $FLAGS @$RSP_FILE -o $TARGET_FILE"
self.rules[APP_LINK_RULE] = {
"command": f"$PRE_LINK && {link} && $POST_BUILD",
"rspfile": "$RSP_FILE",
"rspfile_content": "$in_newline $LINK_PATH $LINK_LIBRARIES",
}
else:
link = (f"{linker} $FLAGS $in -o $TARGET_FILE $LINK_PATH "
"$LINK_LIBRARIES")
wrapped = (f": && {link} && :"
if gcc
else f"$PRE_LINK && {link} && $POST_BUILD")
self.rules[APP_LINK_RULE] = {"command": wrapped}
self.rules[TOOL_LINK_RULE] = {"command": f"{linker} $in -o $out"}
self.add_pch(APP_HEADER, APP_RULE, APP_DEFINES, APP_OPTIONS)
self.add_object(APP_MAIN, APP_RULE, self.source("app/main.cpp"),
APP_DEFINES, APP_OPTIONS, APP_HEADER)
self.add_object(APP_COMMON, APP_RULE, self.source("shared/common.cpp"),
APP_DEFINES, APP_OPTIONS, APP_HEADER)
self.add_object(APP_PLAIN, APP_C_RULE, self.source("app/plain.c"),
APP_DEFINES, APP_OPTIONS)
self.add_pch(LIB_HEADER, LIB_RULE, LIB_DEFINES)
self.add_object(LIB_UTIL, LIB_RULE, self.source("lib dir/util.cpp"),
LIB_DEFINES, header=LIB_HEADER)
self.add_object(TOOL_COMMON, TOOL_RULE,
self.source("shared/common.cpp"), TOOL_DEFINES)
self.edges["Debug/tool"] = {
"rule": TOOL_LINK_RULE,
"explicit": [TOOL_COMMON],
"implicit": [],
"order_only": [],
"vars": {},
}
self.edges["Debug/app"] = {
"rule": APP_LINK_RULE,
"explicit": [APP_MAIN, APP_COMMON, APP_PLAIN, LIB_UTIL],
"implicit": [self.prebuilt],
"order_only": ["Debug/tool"],
"vars": {
"PRE_LINK": ":",
"POST_BUILD": ":",
"FLAGS": "-g",
"TARGET_FILE": "Debug/app",
"LINK_PATH": "-L/opt/fx/lib",
"LINK_LIBRARIES": (
f"{shlex.quote(self.prebuilt)} libz.a -lm libz.a"),
"RSP_FILE": "CMakeFiles/app.rsp",
},
}
self.edges["app"] = {
"rule": "phony",
"explicit": ["Debug/app"],
"implicit": [],
"order_only": [],
"vars": {},
}
def link_vars(self):
return self.edges["Debug/app"]["vars"]
def manifest_path(self):
return os.path.join(self.build_dir, "build-Debug.ninja")
def write(self):
lines = []
for name, bindings in self.rules.items():
lines.append(f"rule {name}")
for key, value in bindings.items():
lines.append(f" {key} = {value}")
for output, edge in self.edges.items():
line = f"build {ninja_path(output)}: {edge['rule']}"
for path in edge["explicit"]:
line += " " + ninja_path(path)
if edge["implicit"]:
line += " | " + " ".join(ninja_path(p) for p in edge["implicit"])
if edge["order_only"]:
line += " || " + " ".join(
ninja_path(p) for p in edge["order_only"])
lines.append(line)
for key, value in edge["vars"].items():
lines.append(f" {key} = {ninja_value(value)}")
with open(self.manifest_path(), "w", encoding="utf-8") as file:
file.write("\n".join(lines) + "\n")
with open(os.path.join(self.build_dir, "CMakeCache.txt"), "w",
encoding="utf-8") as file:
file.write("\n".join(self.cache) + "\n")
def resolve_tree(tree, *extra, target="Debug/app", config="Debug",
runner=None):
argv = [
"--build-dir", tree.build_dir,
"--manifest", "build-Debug.ninja",
"--config", config,
"--target", target,
"--ninja", NINJA,
*extra,
]
return ninja_target.resolve(
ninja_target.parse_args(argv),
runner or ninja_target.execute)
def base_args(tree):
return [
"--policy-target", "app",
"--expect-defined", "FX_APP_POLICY",
"--expect-defined", "FX_LEVEL=2",
"--expect-undefined", "FX_TOOL_POLICY",
"--option-macro", "FX_DISABLE_FEATURE=FX_FEATURE_DISABLED",
"--select-source", tree.source("shared/common.cpp"),
"--list-objects",
]
class SplitCommandTests(unittest.TestCase):
def test_accepts_and_lists(self):
cases = (
("a && b", [["a"], ["b"]]),
(": && c++ 'a b.o' -o x && :",
[[":"], ["c++", "a b.o", "-o", "x"], [":"]]),
("cd /x && c++ -o y", [["cd", "/x"], ["c++", "-o", "y"]]),
("cc -Wno-error=#warnings -c x.c",
[["cc", "-Wno-error=#warnings", "-c", "x.c"]]),
('cc -DQ=\\"Debug\\" -c x.c', [["cc", '-DQ="Debug"', "-c", "x.c"]]),
)
for command, expected in cases:
with self.subTest(command=command):
self.assertEqual(ninja_target.split_command(command), expected)
def test_refuses_other_shell_syntax(self):
cases = (
("a ; b", "';'"),
("a || b", "'\\|'"),
("a | b", "'\\|'"),
("a > f", "'>'"),
("a < f", "'<'"),
("a $x", "'\\$'"),
("a `b`", "'`'"),
("a\nb", "'\\\\n'"),
("(a)", "'\\('"),
('a "&&" b', "outside a whitespace-delimited"),
("a & b", "outside a whitespace-delimited"),
("a &&b", "outside a whitespace-delimited"),
("a && && b", "empty simple command"),
("a 'b", "unsupported shell quoting"),
)
for command, message in cases:
with self.subTest(command=command):
with self.assertRaisesRegex(ninja_target.NinjaTargetError, message):
ninja_target.split_command(command)
class LinkGrammarTests(unittest.TestCase):
def parse(self, command, target="Debug/app", build=None):
build = build or pure_build()
entry = ninja_target.CompdbEntry(
build.host, command, "a.o", target)
return ninja_target.parse_link(entry, target, build)
def test_classifies_operands(self):
link = self.parse(
"/usr/bin/c++ -g -arch arm64 -framework Cocoa -weak_framework Metal "
"-Xlinker -dead_strip a.o /work/out/b.o -o /work/out/Debug/app "
"-L/x -lfoo -l bar libz.a libz.a /usr/lib/libssl.so.3 "
"/usr/lib/libc.dylib x.tbd")
self.assertEqual(link.output, "Debug/app")
self.assertEqual(link.objects, ["a.o", "b.o"])
self.assertEqual(link.archives, {"libz.a": 2})
self.assertEqual(
link.shared_libraries,
["/usr/lib/libssl.so.3", "/usr/lib/libc.dylib", "x.tbd"])
self.assertIsNone(link.launcher)
def test_cd_segments_and_launcher(self):
link = self.parse(
"cd /work/out/Telegram && mkdir -p x && cd /work/out && "
"ccache /usr/bin/g++ a.o -o Debug/app && cd /work/out/Telegram && :")
self.assertEqual(link.cwd, "/work/out")
self.assertEqual(link.launcher, "ccache")
self.assertEqual(link.driver, "/usr/bin/g++")
self.assertEqual(link.objects, ["a.o"])
def test_host_namespace_cd(self):
build = pure_build(configured="/usr/src/fx/out", host="/home/u/out")
link = self.parse(
"cd /home/u/out && c++ /usr/src/fx/out/a.o -o Debug/app",
build=build)
self.assertEqual(link.objects, ["a.o"])
def test_refusals(self):
cases = (
("c++ a.o @CMakeFiles/app.rsp -o Debug/app",
"unexpanded response file"),
("c++ a.o notes.txt -o Debug/app", "unsupported link operand"),
("c++ a.o -o Debug/other", "no link command produces"),
("c++ a.o -o Debug/app && c++ b.o -o Debug/app",
"several link commands"),
("/usr/bin/ld a.o -o Debug/app", "unsupported linker driver"),
("cd /elsewhere && c++ a.o -o Debug/app", "not the build directory"),
("cd relative && c++ a.o -o Debug/app", "unsupported cd form"),
("c++ a.o -o Debug/app -o Debug/app", "several -o"),
("c++ a/../a.o -o Debug/app", "'..' component"),
)
for command, message in cases:
with self.subTest(command=command):
with self.assertRaisesRegex(ninja_target.NinjaTargetError, message):
self.parse(command)
class CompileGrammarTests(unittest.TestCase):
def parse(self, command, inputs=(), output="o.o"):
return ninja_target.parse_compile(
compile_entry(command, output),
pure_build(),
list(inputs))
def test_clang_pch_consumer_and_producer(self):
command = self.parse(
"/usr/bin/c++ -DA -Winvalid-pch -Xclang -include-pch -Xclang "
"/work/out/x/cmake_pch.hxx.pch -Xclang -include -Xclang "
"/work/out/x/cmake_pch.hxx -o o.o -c /src/a.cpp")
self.assertEqual(
(command.pch, command.pch_header, command.pch_dialect,
command.emits_pch, command.source),
("x/cmake_pch.hxx.pch", "x/cmake_pch.hxx", "clang", False,
"/src/a.cpp"))
producer = self.parse(
"/usr/bin/c++ -Xclang -emit-pch -Xclang -include -Xclang "
"/work/out/x/cmake_pch.hxx -x c++-header -o x/cmake_pch.hxx.pch "
"-c /work/out/x/cmake_pch.hxx.cxx",
output="x/cmake_pch.hxx.pch")
self.assertTrue(producer.emits_pch)
self.assertEqual(producer.pch_header, "x/cmake_pch.hxx")
self.assertIsNone(producer.pch)
self.assertEqual(producer.source, "x/cmake_pch.hxx.cxx")
def test_gcc_pch_dialect_with_launcher(self):
gcc = ("ccache /usr/bin/g++-13 -Winvalid-pch -include "
"/work/out/x/cmake_pch.hxx -o o.o -c /src/a.cpp")
command = self.parse(
gcc,
["/src/a.cpp", "x/cmake_pch.hxx", "x/cmake_pch.hxx.gch"])
self.assertEqual(command.launcher, "ccache")
self.assertEqual(command.driver, "/usr/bin/g++-13")
self.assertEqual(command.pch, "x/cmake_pch.hxx.gch")
self.assertEqual(command.pch_dialect, "gcc")
self.assertEqual(command.forced_includes, [])
producer = self.parse(
"/usr/bin/g++ -x c++-header -include /work/out/x/cmake_pch.hxx "
"-o x/cmake_pch.hxx.gch -c /work/out/x/cmake_pch.hxx.cxx",
output="x/cmake_pch.hxx.gch")
self.assertTrue(producer.emits_pch)
self.assertEqual(producer.pch_dialect, "gcc")
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
re.escape(
"incoherent PCH for o.o: x/cmake_pch.hxx.gch is used on the "
"command line but is not an implicit graph input")):
self.parse(gcc, ["/src/a.cpp", "x/cmake_pch.hxx"])
forced = self.parse(
gcc.replace("-Winvalid-pch ", ""),
["/src/a.cpp", "x/cmake_pch.hxx"])
self.assertIsNone(forced.pch)
self.assertEqual(forced.forced_includes, ["x/cmake_pch.hxx"])
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
"forced include may change requested macro"):
ninja_target.macro_state(forced, "A")
def test_ordered_definitions(self):
command = self.parse(
"c++ -DX -DE= -DZ=0 -D S -US -DV=1 -UV -DV=2 -U W -DW "
"-UW -DQ=\\\"Debug\\\" -o o.o -c a.cpp")
expected = {
"X": (True, "1"),
"E": (True, ""),
"Z": (True, "0"),
"S": (False, None),
"V": (True, "2"),
"W": (False, None),
"Q": (True, '"Debug"'),
"MISSING": (False, None),
}
for name, state in expected.items():
with self.subTest(name=name):
self.assertEqual(ninja_target.macro_state(command, name), state)
def test_option_values_are_not_definitions(self):
command = self.parse(
"c++ -MT -DX -MF -UY -I -DZ -o o.o -c a.cpp")
self.assertEqual(command.definitions, [])
def test_refusals(self):
cases = (
("c++ -Xclang -DX -o o.o -c a.cpp", "-Xclang -DX"),
("c++ -Xclang -UX -o o.o -c a.cpp", "-Xclang -UX"),
("c++ -Xpreprocessor -DX -o o.o -c a.cpp", "'-Xpreprocessor'"),
("c++ -imacros m.h -o o.o -c a.cpp", "'-imacros'"),
("c++ -Wp,-DX -o o.o -c a.cpp", "'-Wp,-DX'"),
("c++ --define-macro=X -o o.o -c a.cpp", "'--define-macro=X'"),
("c++ -includem.h -o o.o -c a.cpp", "unsupported include form"),
("c++ @flags.rsp -o o.o -c a.cpp", "unexpanded response file"),
("c++ -Xclang -include-pch -Xclang /p1.pch -Xclang -include-pch "
"-Xclang /p2.pch -Xclang -include -Xclang /h -o o.o -c a.cpp",
"second PCH inclusion"),
("c++ -Xclang -include -Xclang /h -o o.o -c a.cpp",
"outside the PCH form"),
("c++ -o o.o -o p.o -c a.cpp", "exactly one -o"),
("c++ -DX -o o.o", "exactly one -o"),
("c++ -o o.o a.cpp", "unexpected compile operand"),
("c++ -o o.o -c a.cpp extra.cpp", "unexpected compile operand"),
("c++ -o o.o -c a.cpp && :", "not a single command"),
("/usr/bin/cl.exe -o o.o -c a.cpp", "unsupported compiler driver"),
("c++ -D1BAD -o o.o -c a.cpp", "unsupported definition"),
)
for command, message in cases:
with self.subTest(command=command):
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
re.escape(message)):
self.parse(command)
class QueryAndCompdbTests(unittest.TestCase):
def test_query_sections(self):
nodes = ninja_target.parse_query(
"Debug/app:\n"
" input: CXX_EXECUTABLE_LINKER__app_Debug\n"
" a.o\n"
" lib dir/b.o\n"
" | /pre built/init.o\n"
" || Debug/tool\n"
" validations:\n"
" check\n"
" outputs:\n"
" all\n"
"x.cpp:\n"
" outputs:\n"
" a.o\n"
" a.o\n"
"check:\n"
" input: phony\n"
" validation for:\n"
" Debug/app\n")
app = nodes["Debug/app"]
self.assertEqual(app.rule, "CXX_EXECUTABLE_LINKER__app_Debug")
self.assertEqual(app.explicit, ["a.o", "lib dir/b.o"])
self.assertEqual(app.implicit, ["/pre built/init.o"])
self.assertEqual(app.order_only, ["Debug/tool"])
self.assertEqual(app.validations, ["check"])
self.assertEqual(app.outputs, ["all"])
self.assertIsNone(nodes["x.cpp"].rule)
self.assertEqual(nodes["x.cpp"].outputs, ["a.o", "a.o"])
self.assertEqual(nodes["check"].validation_for, ["Debug/app"])
def test_query_refusals(self):
cases = (
("a.o:\n inputs: x\n", "unknown ninja -t query line"),
("a.o:\n input: r\n |x.o\n", "unknown ninja -t query input"),
("a.o:\n x.o\n", "entry outside a section"),
(" input: r\n", "outside a node"),
("a.o:\n outputs:\n x\na.o:\n", "repeated ninja -t query header"),
("a.o:\n outputs:\n outputs:\n", "repeated ninja -t query section"),
("garbage\n", "unknown ninja -t query line"),
)
for text, message in cases:
with self.subTest(text=text):
with self.assertRaisesRegex(ninja_target.NinjaTargetError, message):
ninja_target.parse_query(text)
def test_compdb_refusals(self):
record = {"directory": "/b", "command": "c", "file": "f", "output": "o"}
cases = (
([record, dict(record)], "duplicate ninja -t compdb output"),
([record, dict(record, directory="/c", output="p")], "differs"),
([{"directory": "/b", "command": "c", "file": "f"}], "lacks"),
({"a": 1}, "not a JSON list"),
)
for records, message in cases:
with self.subTest(message=message):
with self.assertRaisesRegex(ninja_target.NinjaTargetError, message):
ninja_target.load_compdb(json.dumps(records))
with self.assertRaisesRegex(ninja_target.NinjaTargetError, "malformed"):
ninja_target.load_compdb("[")
def test_tool_allowlist(self):
cases = (
["commands", "Debug/app"],
["compdb"],
["compdb", "RULE"],
["query"],
["query", "-h"],
["targets", "all"],
["inputs", "Debug/app"],
)
for args in cases:
with self.subTest(args=args):
with self.assertRaisesRegex(ninja_target.NinjaTargetError, "refusing"):
ninja_target.ninja_tool_argv("ninja", "/b", "build.ninja", args)
class CacheIdentityAndSelectorTests(unittest.TestCase):
def test_cmake_cache(self):
with tempfile.TemporaryDirectory() as temporary:
path = os.path.join(temporary, "CMakeCache.txt")
with open(path, "w", encoding="utf-8") as file:
file.write(
"# comment\n//doc\n\nA:BOOL=ON\n"
"\"QUOTED NAME\":STRING=x=y\nEMPTY:STRING=\n")
cache = ninja_target.read_cmake_cache(path)
self.assertEqual(cache["A"], ("BOOL", "ON"))
self.assertEqual(cache["QUOTED NAME"], ("STRING", "x=y"))
self.assertEqual(cache["EMPTY"], ("STRING", ""))
for text, message in (
("garbage\n", "malformed CMakeCache.txt line 1"),
("A:BOOL=ON\nA:BOOL=OFF\n", "duplicate CMakeCache.txt entry A")):
with self.subTest(text=text):
with open(path, "w", encoding="utf-8") as file:
file.write(text)
with self.assertRaisesRegex(ninja_target.NinjaTargetError, message):
ninja_target.read_cmake_cache(path)
def test_bool_option_contracts(self):
cases = (
("ON", True),
("on", True),
("TRUE", True),
("YES", True),
("1", True),
("OFF", False),
("False", False),
("NO", False),
("0", False),
)
for value, defined in cases:
with self.subTest(value=value):
expectations, contracts = ninja_target.parse_expectations(
expectation_options(
expect_defined=["CONTROL"],
option_macro=["OPT=MACRO"]),
{"OPT": ("BOOL", value)})
macro = [item for item in expectations if item.macro == "MACRO"][0]
self.assertEqual(macro.defined, defined)
self.assertEqual(
contracts[0]["expect"],
"defined" if defined else "undefined")
def test_expectation_refusals(self):
cache = {"OPT": ("BOOL", "ON"), "LABEL": ("STRING", "ON")}
cases = (
(dict(option_macro=["LABEL=MACRO"]), "not a BOOL"),
(dict(option_macro=["MISSING=MACRO"]), "has no MISSING"),
(dict(option_macro=["OPT"]), "expected OPTION=MACRO"),
(dict(expect_undefined=["X"]), "known-present control"),
(dict(expect_defined=["X"], expect_undefined=["X"]),
"requested twice"),
(dict(expect_defined=["1X"]), "invalid macro name"),
(dict(expect_defined=["X"], policy_target=None),
"require --policy-target"),
)
for values, message in cases:
with self.subTest(values=values):
with self.assertRaisesRegex(ninja_target.NinjaTargetError, message):
ninja_target.parse_expectations(
expectation_options(**values),
cache)
with self.assertRaisesRegex(ninja_target.NinjaTargetError, "not a BOOL"):
ninja_target.parse_expectations(
expectation_options(
expect_defined=["C"],
option_macro=["OPT=MACRO"]),
{"OPT": ("BOOL", "maybe")})
def test_configured_namespace_identity(self):
cases = (
("/usr/src/fx/out/CMakeFiles/a.o", "CMakeFiles/a.o"),
("/usr/src/fx/outer/a.o", "/usr/src/fx/outer/a.o"),
("/usr/src/fx/out", "/usr/src/fx/out"),
("/opt/other/out/a.o", "/opt/other/out/a.o"),
("CMakeFiles/a.o", "CMakeFiles/a.o"),
)
for token, expected in cases:
with self.subTest(token=token):
self.assertEqual(
ninja_target.identity(token, CONTAINER_DIR),
expected)
def test_basename_selectors_refused_before_any_tool(self):
for flag in ("--select-source", "--select-object"):
with self.subTest(flag=flag):
options = ninja_target.parse_args([
"--build-dir", "/nonexistent",
"--manifest", "build-Debug.ninja",
"--config", "Debug",
"--target", "Debug/app",
flag, "common.cpp",
])
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
"a basename is not an identity"):
ninja_target.resolve(options, refuse_runner)
class HandwrittenManifestTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
if NINJA is None:
raise AssertionError("ninja is required on PATH for these tests")
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="ninja target ")
self.addCleanup(self.temporary.cleanup)
def tree(self, name="case", **arguments):
root = os.path.join(self.temporary.name, name)
os.makedirs(root)
return HandTree(root, **arguments)
def assert_base_report(self, tree, result):
self.assertEqual(result["verdict"], "PASS")
objects = result["objects"]
self.assertEqual(
[item["object"] for item in objects["list"]],
[APP_MAIN, APP_COMMON, APP_PLAIN, LIB_UTIL, tree.prebuilt])
self.assertEqual(objects["by_edge"], {"explicit": 4, "implicit": 1})
self.assertEqual(objects["external"], [tree.prebuilt])
self.assertEqual(
objects["by_rule"],
{APP_RULE: 2, APP_C_RULE: 1, LIB_RULE: 1})
self.assertNotIn(
TOOL_COMMON,
[item["object"] for item in objects["list"]])
records = {item["object"]: item for item in objects["list"]}
self.assertEqual(records[APP_MAIN]["pch"], tree.pch(APP_HEADER))
self.assertIsNone(records[APP_PLAIN]["pch"])
self.assertEqual(records[LIB_UTIL]["pch"], tree.pch(LIB_HEADER))
self.assertEqual(
records[APP_COMMON]["source"],
tree.source("shared/common.cpp"))
self.assertEqual(result["archives"], {"libz.a": 2})
self.assertEqual(result["target"]["order_only_inputs"], 1)
pchs = {item["path"]: item for item in result["pchs"]}
self.assertEqual(
{path: (item["rule"], item["consumers"]) for path, item in pchs.items()},
{
tree.pch(APP_HEADER): (APP_RULE, 2),
tree.pch(LIB_HEADER): (LIB_RULE, 1),
})
cohort = result["cohort"]
self.assertEqual(cohort["rules"], {APP_RULE: 2, APP_C_RULE: 1})
self.assertEqual(cohort["pchs"], [tree.pch(APP_HEADER)])
readings = {item["macro"]: item for item in cohort["macros"]}
self.assertEqual(readings["FX_LEVEL"]["values"], ["2"])
self.assertEqual(readings["FX_TOOL_POLICY"]["expected"], "undefined")
self.assertEqual(readings["FX_FEATURE_DISABLED"]["expected"], "undefined")
self.assertEqual(
cohort["option_contracts"][0]["cache"],
"FX_DISABLE_FEATURE:BOOL=OFF")
self.assertNotIn("FX_TOOL_POLICY", cohort["definition_names"])
selection = result["selections"][0]
self.assertEqual(selection["object"], APP_COMMON)
self.assertEqual(selection["rule"], APP_RULE)
self.assertEqual(
selection["excluded_siblings"],
[{
"object": TOOL_COMMON,
"rule": TOOL_RULE,
"consumers": ["Debug/tool"],
}])
def test_clang_base_passes(self):
tree = self.tree()
tree.write()
result = resolve_tree(tree, *base_args(tree))
self.assert_base_report(tree, result)
self.assertEqual(result["build"]["configured"], tree.build_dir)
if os.path.realpath(tree.build_dir) == tree.build_dir:
self.assertIsNone(result["build"]["path_mapping"])
else:
self.assertEqual(
result["build"]["path_mapping"]["validated_by"],
"samefile")
self.assertEqual(
[item["dialect"] for item in result["pchs"]],
["clang", "clang"])
def test_gcc_dialect_passes(self):
tree = self.tree(dialect="gcc")
tree.write()
result = resolve_tree(tree, *base_args(tree))
self.assert_base_report(tree, result)
self.assertTrue(tree.pch(APP_HEADER).endswith(".gch"))
self.assertEqual(
[item["dialect"] for item in result["pchs"]],
["gcc", "gcc"])
self.assertEqual(result["target"]["linker"], "/usr/bin/g++-13")
def test_gcc_pch_use_without_gch_input_is_incoherent(self):
tree = self.tree(dialect="gcc")
tree.edges[APP_MAIN]["implicit"] = [APP_HEADER]
tree.write()
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
re.escape(
f"incoherent PCH for {APP_MAIN}: {tree.pch(APP_HEADER)} is "
"used on the command line but is not an implicit graph "
"input")):
resolve_tree(tree, *base_args(tree))
def test_gcc_plain_forced_include_refuses_macro_request(self):
tree = self.tree(dialect="gcc")
tree.edges[APP_MAIN]["implicit"] = [APP_HEADER]
flags = tree.edges[APP_MAIN]["vars"]["FLAGS"]
self.assertIn("-Winvalid-pch ", flags)
tree.edges[APP_MAIN]["vars"]["FLAGS"] = flags.replace(
"-Winvalid-pch ", "")
tree.write()
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
"forced include may change requested macro"):
resolve_tree(tree, *base_args(tree))
def test_response_file_link_expands(self):
tree = self.tree(rsp=True)
tree.write()
with open(tree.manifest_path(), encoding="utf-8") as file:
self.assertIn("rspfile_content", file.read())
result = resolve_tree(tree, *base_args(tree))
self.assert_base_report(tree, result)
def test_container_namespace_mapping(self):
tree = self.tree(configured=CONTAINER_DIR)
tree.write()
result = resolve_tree(tree, *base_args(tree))
self.assert_base_report(tree, result)
mapping = result["build"]["path_mapping"]
self.assertEqual(mapping["configured"], CONTAINER_DIR)
self.assertEqual(mapping["validated_by"], "CMakeCache.txt CMAKE_CACHEFILE_DIR")
self.assertTrue(os.path.samefile(mapping["host"], tree.build_dir))
def test_container_namespace_tokens(self):
cases = (
(f"{CONTAINER_DIR}/{APP_MAIN}", "duplicate object operand"),
(f"/opt/other/out/{APP_MAIN}",
"substituted or extra link operand /opt/other/out/"),
)
for token, message in cases:
with self.subTest(token=token):
tree = self.tree(name=message[:9], configured=CONTAINER_DIR)
tree.link_vars()["LINK_LIBRARIES"] += " " + token
tree.write()
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
re.escape(message)):
resolve_tree(tree, *base_args(tree))
def test_configured_dir_that_is_another_local_dir_refuses(self):
other = os.path.join(self.temporary.name, "other")
os.makedirs(other)
tree = self.tree(configured=other)
tree.write()
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
"exists locally but is not the build directory"):
resolve_tree(tree, *base_args(tree))
def test_one_mutation_negatives(self):
def link_libraries(extra):
def mutate(tree):
tree.link_vars()["LINK_LIBRARIES"] += " " + extra
return mutate
def drop_prebuilt(tree):
tree.link_vars()["LINK_LIBRARIES"] = "libz.a -lm libz.a"
def order_only_sibling(tree):
tree.edges["Debug/app"]["order_only"].append(TOOL_COMMON)
tree.link_vars()["LINK_LIBRARIES"] += " " + TOOL_COMMON
def main_rule(rule):
def mutate(tree):
tree.edges[APP_MAIN]["rule"] = rule
tree.edges[APP_MAIN]["vars"]["COMMAND"] = ":"
return mutate
def main_flags(flags):
def mutate(tree):
tree.edges[APP_MAIN]["vars"]["FLAGS"] = flags(tree)
return mutate
def pch_edge(key, value):
def mutate(tree):
edge = tree.edges[tree.pch(APP_HEADER)]
if key == "FLAGS":
edge["vars"]["FLAGS"] = value(tree)
else:
edge[key] = value
return mutate
def drop_main_pch_input(tree):
tree.edges[APP_MAIN]["implicit"] = [APP_HEADER]
def plain_intdir(tree):
tree.edges[APP_PLAIN]["vars"]["DEFINES"] = APP_DEFINES.replace(
"Debug",
"Release")
def post_build(tree):
tree.link_vars()["POST_BUILD"] = "echo linked; :"
def ambiguous_source(tree):
tree.add_object(LIB_COMMON, LIB_RULE,
tree.source("shared/common.cpp"), LIB_DEFINES, header=LIB_HEADER)
tree.edges["Debug/app"]["explicit"].append(LIB_COMMON)
def no_mutation(tree):
pass
second_pch = (lambda tree: tree.edges[APP_MAIN]["vars"]["FLAGS"]
+ " -Xclang -include-pch -Xclang "
+ shlex.quote(tree.absolute(tree.pch(APP_HEADER))))
policy = ["--policy-target", "app"]
cases = (
("duplicate object operand",
link_libraries(APP_MAIN), None,
f"duplicate object operand in link command for Debug/app: "
f"['{APP_MAIN}']"),
("graph input missing from argv", drop_prebuilt, None,
"missing link entry for graph input(s) of Debug/app"),
("substituted sibling object", link_libraries(TOOL_COMMON), None,
f"substituted or extra link operand {TOOL_COMMON}: not an"),
("object only order-only", order_only_sibling, None,
f"substituted or extra link operand {TOOL_COMMON} "
"(order-only only)"),
("producer output mismatch",
main_rule("CXX_COMPILER__app_wrongout_Debug"), None,
f"producer/output mismatch for {APP_MAIN}"),
("producer source mismatch",
main_rule("CXX_COMPILER__app_wrongsrc_Debug"), None,
f"producer/source mismatch for {APP_MAIN}"),
("non-compile producer", main_rule("CUSTOM_COMMAND"), None,
f"non-compile producer rule CUSTOM_COMMAND for object {APP_MAIN}"),
("PCH used but not a graph input", drop_main_pch_input, None,
"is used on the command line but is not an implicit graph input"),
("PCH graph input not used",
main_flags(lambda tree: APP_OPTIONS), None,
"is not used on the command line"),
("two include-pch", main_flags(second_pch), None,
f"second PCH inclusion in compile command for {APP_MAIN}"),
("PCH produced by another target rule",
pch_edge("rule", LIB_RULE), None,
f"is produced by rule {LIB_RULE} but used by"),
("PCH producer not emitting",
pch_edge("FLAGS", lambda tree: APP_OPTIONS), None,
"does not emit a PCH"),
("PCH producer built from another header",
pch_edge("FLAGS", lambda tree: APP_OPTIONS
+ " -Xclang -emit-pch -Xclang -include -Xclang "
+ shlex.quote(tree.absolute("CMakeFiles/app.dir/other.hxx"))
+ " -x c++-header"), None,
"CMakeFiles/app.dir/other.hxx (clang)"),
("macro differs between object and PCH",
main_flags(lambda tree: tree.edges[APP_MAIN]["vars"]["FLAGS"]
+ " -UFX_APP_POLICY"), None,
"requested macro FX_APP_POLICY differs between object and PCH: "
f"{APP_MAIN} (undefined)"),
("policy expectation unmet", no_mutation,
lambda tree: policy + ["--expect-defined", "FX_TOOL_POLICY"],
"policy expectation unmet in cohort app: FX_TOOL_POLICY expected "
"defined (--expect-defined); 4 of 4 objects/PCHs differ"),
("absence without known-present control", no_mutation,
lambda tree: policy + ["--expect-undefined", "FX_TOOL_POLICY"],
"an absence expectation needs a known-present control"),
("wrong CMAKE_INTDIR", plain_intdir, None,
f"CMAKE_INTDIR in {APP_PLAIN} is '\"Release\"', expected "
"'\"Debug\"'"),
("unsupported link shell syntax", post_build, None,
"unsupported shell syntax ';' in command"),
("literal response file without rspfile",
link_libraries("@CMakeFiles/app.rsp"), None,
"unexpanded response file '@CMakeFiles/app.rsp' in link command"),
("ambiguous source", ambiguous_source, None,
f"{APP_COMMON}', '{LIB_COMMON}'"),
("sibling object selected", no_mutation,
lambda tree: ["--select-object", TOOL_COMMON],
f"object {TOOL_COMMON} not consumed by Debug/app; its graph "
"consumers: ['Debug/tool']"),
("source not consumed", no_mutation,
lambda tree: [
"--select-source",
tree.absolute("CMakeFiles/app.dir/cmake_pch.hxx.cxx"),
],
"source not consumed by Debug/app"),
)
for number, (name, mutate, args, message) in enumerate(cases):
with self.subTest(name=name):
tree = self.tree(name=f"case {number}")
mutate(tree)
tree.write()
args = base_args(tree) if args is None else args(tree)
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
re.escape(message)):
resolve_tree(tree, *args)
def test_manifest_argument_refusals(self):
tree = self.tree()
tree.write()
cases = (
(dict(config="Release"),
"manifest build-Debug.ninja does not select configuration Release"),
(dict(config="RelWithDebInfo"), "is not in CMAKE_CONFIGURATION_TYPES"),
(dict(target="app"),
"app is a phony alias, not a link edge; pass one of its exact "
"inputs: ['Debug/app']"),
)
for arguments, message in cases:
with self.subTest(arguments=arguments):
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
re.escape(message)):
resolve_tree(tree, **arguments)
def test_missing_compile_entry(self):
tree = self.tree()
tree.write()
def runner(argv):
code, stdout, stderr = ninja_target.execute(argv)
if argv[5:8] == ["-t", "compdb", "-x"]:
records = [
record
for record in json.loads(stdout)
if record["output"] != APP_MAIN
]
stdout = json.dumps(records)
return code, stdout, stderr
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
re.escape(f"missing compile entry for {APP_MAIN}")):
resolve_tree(tree, *base_args(tree), runner=runner)
def test_manifest_touched_during_acquisition(self):
tree = self.tree()
tree.write()
calls = []
def runner(argv):
calls.append(argv)
if argv[5:7] == ["-t", "query"] and len(calls) == 3:
with open(tree.manifest_path(), "a", encoding="utf-8") as file:
file.write("# touched during acquisition\n")
return ninja_target.execute(argv)
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
"build manifests changed during acquisition: build-Debug.ninja"):
resolve_tree(tree, *base_args(tree), runner=runner)
def test_graph_commands_are_never_executed(self):
tree = self.tree()
sentinel = os.path.join(tree.root, "pre link sentinel")
tree.link_vars()["PRE_LINK"] = f"touch {shlex.quote(sentinel)}"
tree.write()
calls = []
def runner(argv):
calls.append(list(argv))
return ninja_target.execute(argv)
result = resolve_tree(tree, *base_args(tree), runner=runner)
self.assertEqual(result["verdict"], "PASS")
self.assertFalse(os.path.exists(sentinel))
kinds = set()
prefix = [NINJA, "-C", tree.build_dir, "-f", "build-Debug.ninja", "-t"]
for argv in calls:
if argv == [NINJA, "--version"]:
kinds.add("--version")
elif argv[:6] == prefix and argv[6:] == ["compdb", "-h"]:
kinds.add("compdb -h")
elif argv[:6] == prefix and argv[6] == "query" and len(argv) > 7:
kinds.add("query")
elif argv[:6] == prefix and argv[6:8] == ["compdb", "-x"]:
kinds.add("compdb -x")
else:
self.fail(f"unexpected ninja invocation {argv}")
self.assertEqual(kinds, {"--version", "compdb -h", "query", "compdb -x"})
CMAKE_TOOLS = ("cmake", "ninja", "c++", "nm")
FIXTURE_FILES = {
"CMakeLists.txt": """cmake_minimum_required(VERSION 3.16)
project(fx C CXX)
option(FX_DISABLE_FEATURE "Disable the fixture feature" OFF)
option(FX_DUPLICATE_OBJECT "Link the object library objects twice" OFF)
set(FX_PREBUILT_OBJECT "" CACHE FILEPATH "Prebuilt plugin-init analogue")
add_subdirectory("lib dir")
add_executable(fx_tool tool/main.cpp shared/common.cpp)
target_compile_definitions(fx_tool PRIVATE FX_TOOL_POLICY)
target_precompile_headers(fx_tool PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_SOURCE_DIR}/tool/tool_pch.h>")
add_executable(fx_app app/main.cpp shared/common.cpp app/plain.c)
target_compile_definitions(fx_app PRIVATE FX_APP_POLICY FX_LEVEL=0)
target_compile_options(fx_app PRIVATE -UFX_LEVEL -DFX_LEVEL=2)
target_precompile_headers(fx_app PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_SOURCE_DIR}/app/app_pch.h>")
if (FX_DISABLE_FEATURE)
target_compile_definitions(fx_app PRIVATE FX_FEATURE_DISABLED)
endif()
target_link_libraries(fx_app PRIVATE fx_objlib "${FX_PREBUILT_OBJECT}")
if (FX_DUPLICATE_OBJECT)
add_library(fx_extra OBJECT "lib dir/extra.cpp")
target_link_libraries(fx_app PRIVATE fx_extra "$<TARGET_OBJECTS:fx_extra>")
endif()
add_dependencies(fx_app fx_tool)
""",
"lib dir/CMakeLists.txt": """add_library(fx_objlib OBJECT util.cpp)
target_compile_definitions(fx_objlib PRIVATE FX_OBJLIB_POLICY=1)
target_precompile_headers(fx_objlib PRIVATE objlib_pch.h)
""",
"lib dir/objlib_pch.h": "#pragma once\n#define FX_OBJLIB_PCH 1\n",
"lib dir/extra.cpp": "extern \"C\" int fx_extra_value() { return 5; }\n",
"lib dir/util.cpp": (
"extern \"C\" int fx_objlib_value() { return FX_OBJLIB_POLICY; }\n"),
"shared/common.cpp": """#ifdef FX_APP_POLICY
#if FX_LEVEL != 2
#error FX_LEVEL must be 2 in fx_app
#endif
extern "C" int fx_app_marker() { return FX_LEVEL; }
#endif
#ifdef FX_TOOL_POLICY
extern "C" int fx_tool_marker() { return 7; }
#endif
""",
"app/app_pch.h": "#pragma once\n#define FX_APP_PCH 1\n",
"app/main.cpp": """extern "C" int fx_app_marker();
extern "C" int fx_objlib_value();
extern "C" int fx_plain_value(void);
extern "C" int fx_prebuilt_init();
int main() {
return fx_app_marker() + fx_objlib_value() + fx_plain_value()
+ fx_prebuilt_init() == 10 ? 0 : 1;
}
""",
"app/plain.c": "int fx_plain_value(void) { return 3; }\n",
"tool/tool_pch.h": "#pragma once\n#define FX_TOOL_PCH 1\n",
"tool/main.cpp": """extern "C" int fx_tool_marker();
int main() { return fx_tool_marker() == 7 ? 0 : 1; }
""",
}
FX_APP_COMMON = "CMakeFiles/fx_app.dir/Debug/shared/common.cpp.o"
FX_TOOL_COMMON = "CMakeFiles/fx_tool.dir/Debug/shared/common.cpp.o"
FX_APP_PCH = "CMakeFiles/fx_app.dir/Debug/cmake_pch.hxx.pch"
FX_OBJLIB_PCH = "lib dir/CMakeFiles/fx_objlib.dir/Debug/cmake_pch.hxx.pch"
class CompiledFixtureTests(unittest.TestCase):
"""fx_app and fx_tool compile the same shared/common.cpp."""
@classmethod
def setUpClass(cls):
if os.environ.get(SKIP_CMAKE_VARIABLE) == "1":
raise unittest.SkipTest(
f"{SKIP_CMAKE_VARIABLE}=1: compiled CMake fixture skipped")
missing = [tool for tool in CMAKE_TOOLS if shutil.which(tool) is None]
if missing:
raise AssertionError(
f"compiled fixture needs {', '.join(missing)} on PATH; set "
f"{SKIP_CMAKE_VARIABLE}=1 only on a host that cannot provide them")
cls.temporary = tempfile.TemporaryDirectory(prefix="ninja target fx ")
root = cls.temporary.name
cls.source_dir = os.path.join(root, "fx source")
for relative, text in FIXTURE_FILES.items():
path = os.path.join(cls.source_dir, relative)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as file:
file.write(text)
prebuilt_dir = os.path.join(root, "pre built")
os.makedirs(prebuilt_dir)
prebuilt_source = os.path.join(prebuilt_dir, "init.cpp")
with open(prebuilt_source, "w", encoding="utf-8") as file:
file.write("extern \"C\" int fx_prebuilt_init() { return 4; }\n")
cls.prebuilt = os.path.join(prebuilt_dir, "init.cpp.o")
cls.check([
shutil.which("c++"), "-c", prebuilt_source, "-o", cls.prebuilt])
cls.trees = {}
variants = (
("off", ["-DFX_DISABLE_FEATURE=OFF"], True),
("on", ["-DFX_DISABLE_FEATURE=ON"], True),
("rsp", [
"-DFX_DISABLE_FEATURE=ON",
"-DCMAKE_NINJA_FORCE_RESPONSE_FILE=ON",
], False),
("duplicate", ["-DFX_DUPLICATE_OBJECT=ON"], False),
)
try:
for name, arguments, build in variants:
build_dir = os.path.join(root, f"build {name}")
cls.check([
"cmake",
"-G", "Ninja Multi-Config",
f"-DCMAKE_MAKE_PROGRAM={shutil.which('ninja')}",
f"-DFX_PREBUILT_OBJECT={cls.prebuilt}",
*arguments,
"-S", cls.source_dir,
"-B", build_dir,
])
if build:
cls.check(["cmake", "--build", build_dir, "--config", "Debug"])
cls.trees[name] = build_dir
except BaseException:
cls.temporary.cleanup()
raise
@classmethod
def tearDownClass(cls):
cls.temporary.cleanup()
@staticmethod
def check(argv):
result = subprocess.run(
argv,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False)
if result.returncode != 0:
raise AssertionError(
f"{argv} failed with exit {result.returncode}:\n{result.stdout}")
return result.stdout
def resolve(self, variant, *extra):
argv = [
"--build-dir", self.trees[variant],
"--manifest", "build-Debug.ninja",
"--config", "Debug",
"--target", "Debug/fx_app",
"--ninja", shutil.which("ninja"),
*extra,
]
return ninja_target.resolve(ninja_target.parse_args(argv))
def app_args(self, macro="FX_FEATURE_DISABLED"):
return [
"--policy-target", "fx_app",
"--option-macro", f"FX_DISABLE_FEATURE={macro}",
"--expect-defined", "FX_APP_POLICY",
"--expect-defined", "FX_LEVEL=2",
"--expect-undefined", "FX_TOOL_POLICY",
"--select-source", os.path.join(self.source_dir, "shared/common.cpp"),
"--list-objects",
]
def symbols(self, variant, relative):
return self.check(["nm", os.path.join(self.trees[variant], relative)])
def test_selects_app_object_and_excludes_tool_sibling(self):
result = self.resolve("off", *self.app_args())
selection = result["selections"][0]
self.assertEqual(selection["object"], FX_APP_COMMON)
self.assertEqual(selection["pch"], FX_APP_PCH)
self.assertEqual(
selection["excluded_siblings"],
[{
"object": FX_TOOL_COMMON,
"rule": "CXX_COMPILER__fx_tool_unscanned_Debug",
"consumers": ["Debug/fx_tool"],
}])
listed = [item["object"] for item in result["objects"]["list"]]
self.assertIn(FX_APP_COMMON, listed)
self.assertNotIn(FX_TOOL_COMMON, listed)
self.assertEqual(result["objects"]["external"], [self.prebuilt])
records = {item["object"]: item for item in result["objects"]["list"]}
self.assertIsNone(records["CMakeFiles/fx_app.dir/Debug/app/plain.c.o"]["pch"])
build_dir = self.trees["off"]
mapping = result["build"]["path_mapping"]
if os.path.realpath(build_dir) == build_dir:
self.assertIsNone(mapping)
else:
self.assertEqual(mapping["configured"], build_dir)
self.assertEqual(mapping["validated_by"], "samefile")
readings = {item["macro"]: item for item in result["cohort"]["macros"]}
self.assertEqual(readings["FX_LEVEL"]["values"], ["2"])
self.assertEqual(readings["FX_FEATURE_DISABLED"]["expected"], "undefined")
self.assertEqual(result["cohort"]["pchs"], [FX_APP_PCH])
selected = self.symbols("off", selection["object"])
sibling = self.symbols("off", FX_TOOL_COMMON)
self.assertIn("fx_app_marker", selected)
self.assertNotIn("fx_tool_marker", selected)
self.assertIn("fx_tool_marker", sibling)
self.assertNotIn("fx_app_marker", sibling)
def test_object_library_cohort_has_its_own_pch(self):
result = self.resolve(
"off",
"--policy-target", "fx_objlib",
"--expect-defined", "FX_OBJLIB_POLICY=1",
"--expect-undefined", "FX_APP_POLICY")
self.assertEqual(result["cohort"]["pchs"], [FX_OBJLIB_PCH])
self.assertEqual(result["cohort"]["objects"], 1)
def test_option_contract_in_both_variants(self):
off = self.resolve("off", *self.app_args())
on = self.resolve("on", *self.app_args())
self.assertEqual(
off["cohort"]["option_contracts"][0]["cache"],
"FX_DISABLE_FEATURE:BOOL=OFF")
self.assertEqual(
on["cohort"]["option_contracts"][0]["cache"],
"FX_DISABLE_FEATURE:BOOL=ON")
readings = {item["macro"]: item for item in on["cohort"]["macros"]}
self.assertEqual(readings["FX_FEATURE_DISABLED"]["expected"], "defined")
self.assertEqual(readings["FX_FEATURE_DISABLED"]["pchs"], 1)
def test_same_name_contract_fails_when_on(self):
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
"policy expectation unmet in cohort fx_app: FX_DISABLE_FEATURE "
"expected defined"):
self.resolve("on", *self.app_args(macro="FX_DISABLE_FEATURE"))
def test_response_file_variant_has_the_same_objects(self):
on = self.resolve("on", *self.app_args())
rsp = self.resolve("rsp", *self.app_args())
self.assertEqual(
[item["object"] for item in rsp["objects"]["list"]],
[item["object"] for item in on["objects"]["list"]])
self.assertEqual(rsp["selections"], on["selections"])
def test_duplicate_object_variant_refuses(self):
with self.assertRaisesRegex(
ninja_target.NinjaTargetError,
"duplicate object operand in link command for Debug/fx_app"):
self.resolve("duplicate")
if __name__ == "__main__":
unittest.main()