Files
NX2navi/tests/pgo_build_test.py
chrg6 2ede7101fa Build game 1.1.1 with the kit's execution profile, and make its instrumented build.
tools/make_game.py, the build players run through the launcher, read no profile: only tools/build_game.py did, which
does not build 1.1.1. A kit with data/pgo-v111.profdata (build/pgo/ in a checkout) now compiles the lifted code at -O2
with it and the runtime too, the prebuilt runtime library included. The profile is left out, and every command and
key is what it was, when the kit has none, when its record names another recomp/ than the kit's, when a compiler of
the build refuses the file in one trial compile before the others start, and with NX2NAVI_PGO=off. The record holds
recomp/ by content, which a kit without git can compare. The object keys hold the profile's content and not the kit's
folder, so a launcher update that moves the kit keeps the objects.

--pgo gen (NX2NAVI_PGO=gen from a launcher) builds the instrumented game into the game folder: it counts into
<out>/pgo, records what it was built from for the merge, and stops at once when the build tools lack the profile
runtime. tools/pgo.py merge takes that record, refuses a game built from another recomp/ and finds llvm-profdata in an
MSYS2 folder too. tools/check_content.py refuses profiles in a commit.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:30:46 -05:00

457 lines
24 KiB
Python

"""The profile-guided build of tools/make_game.py and tools/pgo.py, with a stand-in for the compiler process.
Which flags a build compiles with: a kit without an execution profile (the flags and keys it always had), a kit with one
that fits (the lifted code at -O2 and every compile with the profile), a record that names another recomp/, a record
that is missing or names none, a profile the compiler refuses in the trial compile, NX2NAVI_PGO=off, and an
instrumented build (NX2NAVI_PGO=gen). The object keys: the same profile in a kit at another place keeps them, another
profile changes them. tools/pgo.py: the record a merge writes names the recomp/ a kit compares, a game built from
another recomp/ stops the merge, and a release refuses a profile of another tree. tools/check_content.py refuses
profiles in a commit.
The last test uses the real compiler when MSYS2 UCRT64's clang is installed (and an llvm-profdata, when one is
found): a file that is no profile is refused by the trial compile, a real one is taken. No game files.
Run: python tests/pgo_build_test.py
"""
import contextlib
import io
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / 'tools'))
import check_content # noqa: E402
import make_game as mg # noqa: E402
import pgo # noqa: E402
REFUSAL = ('clang: error: Error in reading profile pgo-v111.profdata: unsupported instrumentation profile format '
'version')
class Compiler:
"""Stands in for subprocess.run in make_game.py: records every command, answers --version, writes the object
and the dependency file of a compile, and refuses a command with -fprofile-use= when refuse is set."""
def __init__(self, refuse=''):
self.refuse = refuse
self.cmds = []
def __call__(self, cmd, **kw):
cmd = [str(c) for c in cmd]
self.cmds.append(cmd)
if '--version' in cmd:
return mock.Mock(returncode=0, stdout='clang version 23.1.5\nTarget: x86_64-w64-windows-gnu\n', stderr='')
if self.refuse and any(c.startswith('-fprofile-use=') for c in cmd):
return mock.Mock(returncode=1, stdout='', stderr=self.refuse + '\n')
if '-o' in cmd:
Path(cmd[cmd.index('-o') + 1]).write_bytes(b'object')
if '-MF' in cmd:
Path(cmd[cmd.index('-MF') + 1]).write_text(
cmd[cmd.index('-o') + 1].replace('\\', '/') + ': ' + cmd[-1].replace('\\', '/') + '\n')
return mock.Mock(returncode=0, stdout='', stderr='')
def trials(self):
return [c for c in self.cmds if any(a.startswith('-fprofile-use=') for a in c)]
class PgoBuildTest(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp(prefix='nx2navi-pgo-'))
self.addCleanup(shutil.rmtree, self.tmp, True)
self.kit = self.make_kit('kit')
self.clang, self.clangxx = self.tmp / 'msys' / 'bin' / 'clang.exe', self.tmp / 'msys' / 'bin' / 'clang++.exe'
self.inc = ['-I', str(self.kit / 'runtime')]
self.cc = Compiler()
env = {k: v for k, v in os.environ.items() if k not in ('NX2NAVI_PGO', 'NX2NAVI_GUEST_OPT')}
for p in (mock.patch.object(mg, 'KIT', self.kit), mock.patch.object(mg, 'WIN', True),
mock.patch.object(mg, 'PGO_MODE', ''), mock.patch.object(mg, 'PGO_IDS', {}),
mock.patch.dict(os.environ, env, clear=True)):
p.start()
self.addCleanup(p.stop)
def make_kit(self, name, lifter='def lift(): return 1\n'):
kit = self.tmp / name
(kit / 'recomp').mkdir(parents=True)
(kit / 'recomp' / 'gen.py').write_text(lifter)
(kit / 'recomp' / 'patches-v111.txt').write_text('# no patches\n')
(kit / 'runtime').mkdir()
return kit
def add_profile(self, kit=None, folder='data', content=b'\xfflprofi\x81 counts', record='fits'):
"""A profile in the kit's data/ with a record that names the kit's recomp/ ('fits'), another one ('other'),
none ('empty'), or without a record (None)."""
kit = kit or self.kit
d = kit.joinpath(*folder.split('/'))
d.mkdir(parents=True, exist_ok=True)
(d / 'pgo-v111.profdata').write_bytes(content)
if record:
digest = {'fits': mg.recomp_digest(kit), 'other': 'f' * 64}.get(record)
(d / 'pgo-v111.json').write_text(json.dumps({'version': 'v111', **({'recomp_digest': digest} if digest else {})}))
return d / 'pgo-v111.profdata'
def choose(self, fast=None):
with mock.patch.object(mg.subprocess, 'run', self.cc):
return mg.pgo_profile(self.clang, {}, self.tmp / 'trial', fast, self.tmp / 'msys')
def commands(self, pgo=()):
"""(the lifted code's command, a runtime file's) with the flags pgo, without the compiler and the paths."""
cmd_for = mg.runtime_cmd(self.clang, self.clangxx, self.inc, level=3, pgo=pgo)
guest = cmd_for(self.tmp / 'main_0001.c', self.tmp / 'main_main_0001.o')
runtime = cmd_for(self.kit / 'runtime' / 'perf.c', self.tmp / 'rt_perf.o')
return guest[1:-5], runtime[1:-5] # up to -I <runtime> -o <object> <source>
PLAIN = (['-c', '-std=gnu11', '-O1', '-march=x86-64-v3', '-fno-strict-aliasing', '-w'],
['-c', '-std=gnu11', '-O2', '-fno-strict-aliasing', '-w'])
def test_a_kit_without_a_profile_builds_as_before(self):
profile, why = self.choose()
self.assertIsNone(profile)
self.assertIn('no execution profile', why)
self.assertEqual(self.cc.cmds, []) # nothing to try
self.assertEqual(self.commands(), self.PLAIN)
self.assertEqual(mg.pgo_tag(), '') # the compile step's key is what it was
self.assertEqual(mg.pgo_salt('compiler', None), 'compiler') # and so are the object keys
with mock.patch.dict(os.environ, {'NX2NAVI_PGO': 'off'}):
self.assertEqual(mg.pgo_tag(), '')
def test_a_fitting_profile_goes_into_every_compile(self):
path = self.add_profile()
fast = self.tmp / 'msys' / 'clang64' / 'bin' / 'clang.exe'
profile, why = self.choose(fast)
self.assertEqual((profile, why), (path, ''))
# one trial compile by each compiler that will read the file, the fast one with its sysroot
trials = self.cc.trials()
self.assertEqual([t[0] for t in trials], [str(self.clang), str(fast)])
self.assertEqual(trials[1][1], f'--sysroot={self.tmp / "msys"}')
self.assertFalse((self.tmp / 'trial').exists())
use = [f'-fprofile-use={path}', '-mllvm', '-static-func-full-module-prefix=false']
self.assertEqual(mg.pgo_use_flags(profile), use)
guest, runtime = self.commands(mg.pgo_use_flags(profile))
self.assertEqual(guest, ['-c', '-std=gnu11', '-O2', '-march=x86-64-v3', '-fno-strict-aliasing', '-w', *use])
self.assertEqual(runtime, [*self.PLAIN[1], *use])
self.assertEqual(mg.pgo_tag(), '|pgo ' + mg.pgo_id(path))
self.assertNotEqual(mg.pgo_salt('compiler', profile), 'compiler')
def test_a_source_checkout_takes_the_profile_from_build_pgo(self):
path = self.add_profile(folder='build/pgo')
self.assertEqual(self.choose(), (path, ''))
release = self.add_profile() # a release's data/ comes first
self.assertEqual(self.choose(), (release, ''))
def test_a_record_of_another_tree_leaves_the_profile_out(self):
self.add_profile(record='other')
profile, why = self.choose()
self.assertIsNone(profile)
self.assertIn('trained on another recomp/', why)
self.assertEqual(self.cc.cmds, []) # decided before any compiler runs
self.assertEqual(self.commands(), self.PLAIN)
# the same record fits once the kit's recomp/ is the one it names, and not after the lifter changed again
self.add_profile(record='fits')
self.assertIsNotNone(self.choose()[0])
(self.kit / 'recomp' / 'gen.py').write_text('def lift(): return 2\n')
self.assertIsNone(self.choose()[0])
(self.kit / 'recomp' / 'patches-v111.txt').write_text('# a patch\n')
self.assertIsNone(self.choose()[0])
def test_a_missing_or_empty_record_leaves_the_profile_out(self):
for record, said in ((None, 'is missing'), ('empty', 'does not name the recomp/')):
shutil.rmtree(self.kit / 'data', ignore_errors=True)
self.add_profile(record=record)
profile, why = self.choose()
self.assertIsNone(profile, record)
self.assertIn(said, why)
(self.kit / 'data' / 'pgo-v111.json').write_text('{ cut short')
self.assertIn('does not name the recomp/', self.choose()[1])
self.assertEqual(self.cc.cmds, [])
def test_a_profile_the_compiler_refuses_is_left_out(self):
self.add_profile()
self.cc = Compiler(refuse=REFUSAL)
profile, why = self.choose()
self.assertIsNone(profile)
self.assertIn('the compiler cannot read it', why)
self.assertIn('unsupported instrumentation profile format version', why)
self.assertEqual(len(self.cc.trials()), 1) # one compile says so, not every compile of the build
self.assertEqual(self.commands(), self.PLAIN)
# the second compiler's refusal counts too
fast = self.tmp / 'msys' / 'clang64' / 'bin' / 'clang.exe'
class OnlyFast(Compiler):
def __call__(self, cmd, **kw):
self.refuse = REFUSAL if str(cmd[0]) == str(fast) else ''
return super().__call__(cmd, **kw)
self.cc = OnlyFast()
self.assertIsNone(self.choose(fast)[0])
self.assertEqual(len(self.cc.trials()), 2)
def test_a_compiler_that_does_not_start_leaves_the_profile_out(self):
self.add_profile()
def gone(cmd, **kw):
raise FileNotFoundError(2, 'No such file or directory', str(cmd[0]))
with mock.patch.object(mg.subprocess, 'run', gone):
profile, why = mg.pgo_profile(self.clang, {}, self.tmp / 'trial')
self.assertIsNone(profile)
self.assertIn('does not start', why)
def test_off_leaves_a_fitting_profile_out(self):
self.add_profile()
for mode in ({'NX2NAVI_PGO': 'off'}, {'NX2NAVI_PGO': ' OFF '}):
with mock.patch.dict(os.environ, mode):
profile, why = self.choose()
self.assertIsNone(profile)
self.assertIn('NX2NAVI_PGO=off', why)
self.assertEqual(mg.pgo_tag(), '|pgo off')
with mock.patch.object(mg, 'PGO_MODE', 'off'): # --pgo off
self.assertIsNone(self.choose()[0])
self.assertEqual(self.cc.cmds, [])
with mock.patch.dict(os.environ, {'NX2NAVI_PGO': 'something'}): # no mode: as without it
self.assertIsNotNone(self.choose()[0])
def test_only_the_windows_build_takes_the_profile(self):
self.add_profile()
with mock.patch.object(mg, 'WIN', False):
profile, why = self.choose()
self.assertIsNone(profile)
self.assertIn('Windows', why)
def test_an_instrumented_build_counts_into_the_game_folder(self):
out = self.tmp / 'game out'
with mock.patch.dict(os.environ, {'NX2NAVI_PGO': 'gen'}):
self.assertEqual(mg.pgo_mode(), 'gen')
self.assertEqual(mg.pgo_tag(), '|pgo gen')
flags = mg.pgo_gen_flags(out)
self.assertEqual(flags, [f'-fprofile-generate={(out / "pgo").as_posix()}', '-mllvm',
'-static-func-full-module-prefix=false', '-DNX2NAVI_PGO_GEN'])
guest, runtime = self.commands(flags)
self.assertEqual(guest, ['-c', '-std=gnu11', '-O2', '-march=x86-64-v3', '-fno-strict-aliasing', '-w', *flags])
self.assertEqual(runtime, [*self.PLAIN[1], *flags])
# what the game was built from goes beside the counts, and tools/pgo.py reads it back
self.make_real_recomp()
with mock.patch.object(mg.subprocess, 'run', self.cc):
mg.pgo_gen_record(out, self.clang, {})
made = json.loads((out / 'pgo' / 'training.json').read_text())
self.assertEqual(made['recomp_digest'], mg.recomp_digest())
self.assertEqual(made['compiler'], 'clang version 23.1.5')
self.assertEqual(pgo.training(out, 'v111'), made)
def make_real_recomp(self):
"""The test kit's recomp/ becomes this checkout's, so that what make_game.py records in the kit is what
tools/pgo.py, which reads the checkout, compares it with."""
shutil.rmtree(self.kit / 'recomp')
(self.kit / 'recomp').mkdir()
for p in (ROOT / 'recomp').iterdir():
if p.suffix == '.py' or p.name == 'patches-v111.txt':
shutil.copy2(p, self.kit / 'recomp' / p.name)
def test_build_tools_without_the_profile_runtime_stop_an_instrumented_build(self):
def no_runtime(cmd, **kw):
self.assertIn('-fprofile-generate', cmd)
return mock.Mock(returncode=1, stdout='', stderr='ld: cannot find libclang_rt.profile.a: No such file\n'
'clang++: error: linker command failed\n')
msys = self.tmp / 'msys64' / 'ucrt64'
err = io.StringIO()
with mock.patch.object(mg.subprocess, 'run', no_runtime), contextlib.redirect_stderr(err), \
self.assertRaises(SystemExit):
mg.pgo_gen_check(self.clangxx, {}, self.tmp / 'trial', msys, ['-fuse-ld=lld'])
self.assertIn('mingw-w64-ucrt-x86_64-compiler-rt', err.getvalue())
self.assertIn('cannot find libclang_rt.profile.a', err.getvalue())
self.assertFalse((self.tmp / 'trial').exists())
with mock.patch.object(mg.subprocess, 'run', self.cc): # with the runtime: nothing to say
mg.pgo_gen_check(self.clangxx, {}, self.tmp / 'trial', msys, [])
def test_the_object_keys_follow_the_profile_and_not_the_kits_folder(self):
"""A launcher update puts the kit into another folder: the same profile there keeps every object. Another
profile, or a build that leaves it out, compiles again."""
src = self.tmp / 'lift' / 'main_0001.c'
src.parent.mkdir()
src.write_text('int f;\n')
obj = self.tmp / 'obj'
obj.mkdir()
def key(kit, profile):
cache = mg.ObjectCache(obj / 'objects.json', {'<kit>': kit})
inc = ['-I', str(kit / 'runtime')]
cmd_for = mg.runtime_cmd(self.clang, self.clangxx, inc, level=3,
pgo=mg.pgo_use_flags(profile) if profile else ())
dst = obj / 'main_main_0001.o'
return mg.object_key(cmd_for(src, dst), src, dst, cache, mg.pgo_salt('compiler', profile))
here = self.add_profile()
moved = self.make_kit('kit 2')
there = self.add_profile(moved)
self.assertEqual(key(self.kit, here), key(moved, there))
self.assertNotEqual(key(self.kit, here), key(self.kit, None))
other = self.add_profile(moved, content=b'\xfflprofi\x81 other counts')
self.assertNotEqual(key(self.kit, here), key(moved, other))
# without a profile the key is the one a kit before profiles gave: the flags and the compiler's id alone
cache = mg.ObjectCache(obj / 'objects.json', {'<kit>': self.kit})
dst = obj / 'main_main_0001.o'
plain = [str(self.clang), *self.PLAIN[0], '-I', str(self.kit / 'runtime'), '-o', str(dst), str(src)]
self.assertEqual(key(self.kit, None), mg.object_key(plain, src, dst, cache, 'compiler'))
def test_a_new_profile_compiles_the_objects_again(self):
src, obj = self.tmp / 'src', self.tmp / 'obj'
src.mkdir()
obj.mkdir()
srcs = []
for i in range(4):
(src / f'main_{i}.c').write_text(f'int f{i};\n')
srcs.append((src / f'main_{i}.c', obj / f'main_main_{i}.o'))
def build():
profile, _ = mg.pgo_profile(self.clang, {}, self.tmp / 'trial')
cache = mg.ObjectCache(obj / 'objects.json', {'<kit>': self.kit})
cmd_for = mg.runtime_cmd(self.clang, self.clangxx, self.inc, level=3,
pgo=mg.pgo_use_flags(profile) if profile else ())
return mg.compile_all(srcs, cmd_for, cache, 2, {}, salt=mg.pgo_salt('compiler', profile))[:2]
with mock.patch.object(mg.subprocess, 'run', self.cc):
self.assertEqual(build(), (4, 0)) # no profile
self.assertEqual(build(), (0, 4))
self.add_profile()
self.assertEqual(build(), (4, 0)) # the kit brought one
self.assertEqual(build(), (0, 4))
self.add_profile(content=b'\xfflprofi\x81 counts of a longer session')
self.assertEqual(build(), (4, 0)) # and another
self.add_profile(content=b'\xfflprofi\x81 counts of a longer session', record='other')
self.assertEqual(build(), (4, 0)) # left out: the flags of a kit without one
self.assertEqual(build(), (0, 4))
compiles = [c for c in self.cc.cmds if '-MF' in c]
self.assertTrue(all('-O1' in c for c in compiles[:4] + compiles[-4:]))
self.assertTrue(all('-O2' in c and any(a.startswith('-fprofile-use=') for a in c) for c in compiles[4:12]))
class PgoToolTest(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp(prefix='nx2navi-pgo-'))
self.addCleanup(shutil.rmtree, self.tmp, True)
self.build = self.tmp / 'game'
(self.build / 'pgo').mkdir(parents=True)
(self.build / 'pgo' / 'default_1.profraw').write_bytes(b'raw counts')
self.tool = self.tmp / 'llvm' / 'bin' / ('llvm-profdata' + pgo.EXE)
self.tool.parent.mkdir(parents=True)
self.tool.write_bytes(b'')
self.ran = []
p = mock.patch.object(pgo, 'recomp_tree', lambda: 'a' * 40)
p.start()
self.addCleanup(p.stop)
def profdata(self, cmd, **kw):
"""Stands in for llvm-profdata."""
cmd = [str(c) for c in cmd]
self.ran.append(cmd)
if 'merge' in cmd:
Path(cmd[cmd.index('-o') + 1]).write_bytes(b'merged')
return mock.Mock(returncode=0, stdout='LLVM (http://llvm.org/):\n LLVM version 23.1.5\n', stderr='')
def merge(self, version='v111'):
out = self.tmp / 'pgo' / f'pgo-{version}.profdata'
with mock.patch.object(pgo.subprocess, 'run', self.profdata), contextlib.redirect_stdout(io.StringIO()):
pgo.merge(self.build, out, version, self.tool.parent.parent)
return out
def test_the_merge_records_the_recomp_a_kit_compares(self):
out = self.merge()
self.assertEqual(out.read_bytes(), b'merged')
self.assertEqual(self.ran[0][:3], [str(self.tool), 'merge', '--failure-mode=all'])
self.assertEqual(self.ran[0][-1], str(self.build / 'pgo' / 'default_1.profraw'))
record = json.loads(out.with_suffix('.json').read_text())
self.assertEqual(record['version'], 'v111')
self.assertEqual(record['recomp_tree'], 'a' * 40)
self.assertEqual(record['tool'], 'LLVM version 23.1.5')
# what make_game.py works out for a kit with this checkout's recomp/: the profile fits such a kit
with mock.patch.object(mg, 'KIT', ROOT):
self.assertEqual(record['recomp_digest'], mg.recomp_digest())
data = self.tmp / 'release' / 'data'
placed = pgo.place(out, data)
self.assertEqual(mg.pgo_record(placed), '')
self.assertEqual(sorted(p.name for p in data.iterdir()), ['pgo-v111.json', 'pgo-v111.profdata'])
def test_a_game_built_from_another_recomp_stops_the_merge(self):
made = {'version': 'v111', 'recomp_digest': pgo.recomp_digest('v111'), 'compiler': 'clang version 23.1.5'}
(self.build / 'pgo' / 'training.json').write_text(json.dumps(made))
record = json.loads(self.merge().with_suffix('.json').read_text())
self.assertEqual(record['compiler'], 'clang version 23.1.5')
(self.build / 'pgo' / 'training.json').write_text(json.dumps(dict(made, recomp_digest='0' * 64)))
with self.assertRaises(SystemExit) as e:
self.merge()
self.assertIn('another recomp/', str(e.exception))
(self.build / 'pgo' / 'training.json').write_text(json.dumps(dict(made, version='v143')))
with self.assertRaises(SystemExit):
self.merge()
def test_a_merge_needs_counts_and_a_clean_recomp(self):
(self.build / 'pgo' / 'default_1.profraw').unlink()
with self.assertRaises(SystemExit) as e:
self.merge()
self.assertIn('no .profraw', str(e.exception))
(self.build / 'pgo' / 'default_1.profraw').write_bytes(b'raw counts')
with mock.patch.object(pgo, 'recomp_tree', lambda: None), self.assertRaises(SystemExit) as e:
self.merge()
self.assertIn('uncommitted changes', str(e.exception))
def test_a_release_refuses_a_profile_of_another_tree(self):
out = self.merge()
rec = out.with_suffix('.json')
record = json.loads(rec.read_text())
self.assertEqual(pgo.fitting(out), record)
rec.write_text(json.dumps(dict(record, recomp_digest='0' * 64)))
with self.assertRaises(SystemExit) as e:
pgo.place(out, self.tmp / 'release' / 'data')
self.assertIn('another recomp/', str(e.exception))
self.assertFalse((self.tmp / 'release').exists())
rec.write_text(json.dumps(dict(record, version='v143')))
with self.assertRaises(SystemExit):
pgo.fitting(out)
rec.unlink()
with self.assertRaises(SystemExit):
pgo.fitting(out)
def test_profiles_stay_out_of_a_commit(self):
self.assertTrue(check_content.execution_profile(Path('data/pgo-v111.profdata')))
self.assertTrue(check_content.execution_profile(Path('game/pgo/default_1.PROFRAW')))
self.assertFalse(check_content.execution_profile(Path('data/pgo-v111.json')))
self.assertFalse(check_content.execution_profile(Path('tools/pgo.py')))
MSYS = Path(r'C:\msys64\ucrt64')
TEXT_PROFILE = ':ir\nnx2navi_pgo_trial\n0\n1\n100\n\n' # one function with one counter, as llvm-profdata reads text
@unittest.skipUnless((MSYS / 'bin' / 'clang.exe').is_file(), 'needs MSYS2 UCRT64 clang')
class RealCompilerTest(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp(prefix='nx2navi-pgo-'))
self.addCleanup(shutil.rmtree, self.tmp, True)
self.clang = MSYS / 'bin' / 'clang.exe'
self.env = dict(os.environ, PATH=str(MSYS / 'bin') + os.pathsep + os.environ.get('PATH', ''))
def test_the_trial_compile_tells_a_profile_from_a_file_that_is_none(self):
bad = self.tmp / 'pgo-v111.profdata'
bad.write_bytes(b'not a profile')
said = mg.pgo_trial(bad, [[self.clang]], self.env, self.tmp / 'trial')
self.assertIn('Error in reading profile', said)
cut = self.tmp / 'cut.profdata'
cut.write_bytes(b'\xfflprofi\x81') # a profile's first bytes and nothing after them
self.assertTrue(mg.pgo_trial(cut, [[self.clang]], self.env, self.tmp / 'trial'))
self.assertTrue(mg.pgo_trial(self.tmp / 'missing.profdata', [[self.clang]], self.env, self.tmp / 'trial'))
tool = shutil.which('llvm-profdata') or next(
(str(p) for p in (MSYS / 'bin' / 'llvm-profdata.exe', Path(r'C:\Program Files\LLVM\bin\llvm-profdata.exe'))
if p.is_file()), None)
if not tool:
self.skipTest('no llvm-profdata to write a real profile with; the refusals above were checked')
text, good = self.tmp / 'trial.proftext', self.tmp / 'good.profdata'
text.write_text(TEXT_PROFILE)
subprocess.run([tool, 'merge', '-o', str(good), str(text)], check=True, capture_output=True)
self.assertEqual(mg.pgo_trial(good, [[self.clang]], self.env, self.tmp / 'trial'), '')
if __name__ == '__main__':
unittest.main()