repo: merge the swap brightness path (backlight-path)
next2c showed WindowServer bind the panel to AdrenoBacklight but its sandbox refuse every brightness-nits write (iokit-set-properties is allowed only on AppleARMBacklight). The brightness now comes from the swap record IOMobileFramebufferSwapSetBrightness fills (+0x354 present, +0x35e nits as a double): AdrenoDisplay overrides swap_submit and hands the value to AdrenoBacklight as source swap, at most one monitor call per 33 ms. force-brightness-control is no longer published (SnapAdreno e1cf072).
This commit is contained in:
commit
6e634e62e5
6 files changed
+158
-29
No files matched your search
@@ -233,6 +233,27 @@ static inline uint32_t edp_bl_nits_to_slider(uint64_t nits_q16, uint64_t lo_q16,
|
||||
if (nits_q16 >= hi_q16) return EDP_BL_SLIDER_MAX;
|
||||
return (uint32_t)(((nits_q16 - lo_q16) * EDP_BL_SLIDER_MAX + (hi_q16 - lo_q16) / 2) / (hi_q16 - lo_q16));
|
||||
}
|
||||
/* A swap record's brightness (an IEEE 754 binary64 in nits, as IOMobileFramebufferSwapSetBrightness stores it) as
|
||||
* 16.16 nits, rounded to nearest and capped at cap_q16, in integer arithmetic only: the bits come straight from a
|
||||
* client's record. 0 with *q16 set; -1 for a NaN, an infinity or a negative value, which name no brightness. Zero and
|
||||
* subnormals are 0 nits (the darkest request). */
|
||||
static inline int edp_bl_double_to_q16(uint64_t bits, uint64_t cap_q16, uint64_t *q16)
|
||||
{
|
||||
/* A normal value is mantissa * 2^(exponent - 1075), so value * 2^16 = mantissa * 2^(exponent - 1059). */
|
||||
uint32_t exponent = (uint32_t)(bits >> 52) & 0x7ffu;
|
||||
uint64_t mantissa = (bits & ((1ull << 52) - 1)) | 1ull << 52, v;
|
||||
if (exponent == 0x7ffu || (bits >> 63 && bits << 1)) return -1;
|
||||
if (exponent == 0)
|
||||
v = 0;
|
||||
else if (exponent >= 1059) /* 2^36 nits or more; a mantissa below 2^53 shifted by up to 10 stays below 2^63 */
|
||||
v = exponent - 1059 > 10 ? UINT64_MAX : mantissa << (exponent - 1059);
|
||||
else if (1059 - exponent > 63)
|
||||
v = 0;
|
||||
else
|
||||
v = (mantissa + (1ull << (1059 - exponent - 1))) >> (1059 - exponent);
|
||||
*q16 = v > cap_q16 ? cap_q16 : v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- Allow-lists: every MMIO access and every DPCD transaction the monitor makes passes one of these ----------- */
|
||||
static inline int edp_bl_mmio_read_ok(uint64_t pa)
|
||||
|
||||
+5
-4
@@ -202,10 +202,11 @@ DPCD `0x721`-`0x723`, clamping the level to between 2% of full scale and the
|
||||
panel's maximum, and stopping for the boot on any failure with the brightness
|
||||
left as it was (`compat/edp-backlight.h`, event `EV_EDP_BACKLIGHT`). The guest
|
||||
gets the AuxKC pinned as `INPUT_AUXKC_DESKTOP_BACKLIGHT`, whose AdrenoX2 has the
|
||||
`AdrenoBacklight` service, and the boot-arg `adreno_backlight=1`. CoreBrightness
|
||||
hands its nits to WindowServer, whose BrightnessControl writes them to the
|
||||
service as `brightness-nits` because the service carries
|
||||
`force-brightness-control` (the path and its evidence are in the kext's
|
||||
`AdrenoBacklight` service, and the boot-arg `adreno_backlight=1`. The service
|
||||
lets WindowServer bind the panel's framebuffer to a brightness control;
|
||||
CoreBrightness's nits then arrive in the swaps WindowServer submits (as on a
|
||||
DCP Mac), and AdrenoDisplay hands each new value to the service, which logs
|
||||
`SET <level> from swap` (the path and its evidence are in the kext's
|
||||
`src/AdrenoBacklight.h`). The service maps nits to a level proportionally, the
|
||||
top level being `adreno_backlight_max_nits` (default 500) and the darkest
|
||||
request `adreno_backlight_min_nits` (default 5, raised to the 2% floor's ~10
|
||||
|
||||
+1
-1
Submodule gpu/adreno-kext/kext updated: 9cc1e92ba9...e1cf0729c6.
@@ -24,7 +24,7 @@ Writes <out>/iomfb-vtable-check.json (the full per-slot table) and exits 1 on an
|
||||
Usage: tools/check-iomfb-vtable.py [--out DIR] [--kc PATH] [--kdk PATH] [--no-display] [--display-define NAME[=V] ...]
|
||||
--display-define (alias --define) compiles AdrenoDisplay.cpp, and only it, with that define too: a profile's
|
||||
AdrenoDisplay set, or an opt-in override such as ADRENO_DISPLAY_TRACE_MODES=1 or ADRENO_BACKLIGHT=1 (whose
|
||||
set_brightness_gated override must then pass the same slot check); without it the default build is checked.
|
||||
set_brightness_gated and swap_submit overrides must then pass the same slot check); without it the default build is checked.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
|
||||
@@ -242,6 +242,54 @@ static void test_curve(void)
|
||||
CHECK(edp_bl_nits_to_level(edp_bl_slider_to_nits(65536, lo, Q(500)), maxn, floor, max) == max);
|
||||
}
|
||||
|
||||
/* A swap record's brightness double as 16.16 nits: bit-exact against the host's own double arithmetic, refusing what
|
||||
* names no brightness, capped, and never trusting the bits to be a sensible number. */
|
||||
static uint64_t bits_of(double d) { uint64_t b; memcpy(&b, &d, sizeof b); return b; }
|
||||
static void test_swap_double(void)
|
||||
{
|
||||
const uint64_t cap = (uint64_t)EDP_BL_NITS_LIMIT_MAX << 16;
|
||||
uint64_t q = 12345;
|
||||
static const double exact[] = {0.0, 1.0, 5.0, 10.015, 140.0, 174.89, 384.615, 499.999, 500.0, 0.5, 1.0 / 65536,
|
||||
1.5 / 65536, 0.49 / 65536, 1e-300, 999.99998};
|
||||
for (unsigned i = 0; i < sizeof exact / sizeof exact[0]; i++) {
|
||||
double diff;
|
||||
CHECK(edp_bl_double_to_q16(bits_of(exact[i]), cap, &q) == 0);
|
||||
diff = (double)q - exact[i] * 65536.0;
|
||||
CHECK(diff <= 0.5 && diff >= -0.5);
|
||||
}
|
||||
CHECK(edp_bl_double_to_q16(bits_of(140.0), cap, &q) == 0 && q == 140ull << 16);
|
||||
CHECK(edp_bl_double_to_q16(bits_of(-0.0), cap, &q) == 0 && q == 0);
|
||||
CHECK(edp_bl_double_to_q16(bits_of(4.9e-324), cap, &q) == 0 && q == 0); /* subnormal */
|
||||
CHECK(edp_bl_double_to_q16(bits_of(1000.5), cap, &q) == 0 && q == cap);
|
||||
CHECK(edp_bl_double_to_q16(bits_of(1e300), cap, &q) == 0 && q == cap);
|
||||
CHECK(edp_bl_double_to_q16(bits_of(68719476736.0), cap, &q) == 0 && q == cap); /* 2^36: the shifted branch */
|
||||
CHECK(edp_bl_double_to_q16(bits_of(1e300), UINT64_MAX, &q) == 0 && q == UINT64_MAX);
|
||||
CHECK(edp_bl_double_to_q16(bits_of(68719476736.0), UINT64_MAX, &q) == 0 && q == 1ull << 52);
|
||||
q = 777;
|
||||
CHECK(edp_bl_double_to_q16(0x7ff8000000000000ull, cap, &q) == -1 && q == 777); /* the NaN CoreAnimation logs */
|
||||
CHECK(edp_bl_double_to_q16(0x7ff0000000000001ull, cap, &q) == -1); /* signalling NaN */
|
||||
CHECK(edp_bl_double_to_q16(0x7ff0000000000000ull, cap, &q) == -1); /* +inf */
|
||||
CHECK(edp_bl_double_to_q16(0xfff0000000000000ull, cap, &q) == -1); /* -inf */
|
||||
CHECK(edp_bl_double_to_q16(bits_of(-1.0), cap, &q) == -1 && edp_bl_double_to_q16(bits_of(-1e-300), cap, &q) == -1);
|
||||
/* Every exponent and a spread of mantissas: monotonic in the value, never above the cap, rounding within half a
|
||||
* step of the host's arithmetic. */
|
||||
uint64_t seed = 0x9e3779b97f4a7c15ull, prev = 0;
|
||||
for (uint32_t e = 0; e < 0x7ff; e++) {
|
||||
for (unsigned k = 0; k < 8; k++) {
|
||||
seed = seed * 6364136223846793005ull + 1442695040888963407ull;
|
||||
uint64_t frac = k == 0 ? 0 : k == 1 ? (1ull << 52) - 1 : seed >> 12;
|
||||
uint64_t b = (uint64_t)e << 52 | frac;
|
||||
double d;
|
||||
memcpy(&d, &b, sizeof d);
|
||||
CHECK(edp_bl_double_to_q16(b, cap, &q) == 0 && q <= cap);
|
||||
double want = d * 65536.0;
|
||||
if (want < (double)cap) CHECK((double)q >= want - 0.5 && (double)q <= want + 0.5);
|
||||
else CHECK(q == cap);
|
||||
if (k == 0) { CHECK(q >= prev); prev = q; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void test_helpers(void)
|
||||
{
|
||||
uint32_t bits = 0;
|
||||
@@ -272,6 +320,7 @@ static void test_helpers(void)
|
||||
CHECK(edp_bl_encode(0x5A3, 1, b) == 2 && b[0] == 0x05 && b[1] == 0xA3 && edp_bl_decode(b, 1) == 0x5A3);
|
||||
CHECK(edp_bl_encode(0xC8, 0, b) == 1 && b[0] == 0xC8 && edp_bl_decode(b, 0) == 0xC8);
|
||||
test_curve();
|
||||
test_swap_double();
|
||||
uint8_t caps[4] = {0x03, 0x07, 0x06, 0x10};
|
||||
uint64_t x1 = edp_bl_reply_x1(EDP_BL_STATE_MODE, EDP_BL_DETAIL_BITS, caps, 0x12);
|
||||
CHECK(edp_bl_reply_abi(x1) == EDP_BL_ABI && edp_bl_reply_state(x1) == EDP_BL_STATE_MODE &&
|
||||
|
||||
+81
-23
@@ -13,12 +13,14 @@ safety property or bends the curve in a copy of the engine; the same program mus
|
||||
|
||||
Wiring: probes/xnuboot.c defaults SNAP_EDP_BACKLIGHT to 0 and every monitor use sits under it; compat/edp-backlight.c is
|
||||
linked only through probes/run.py OPTIONAL; the call number and event number are unique; the kext's backlight code sits
|
||||
under ADRENO_BACKLIGHT and publishes what BrightnessControl needs to write to it (force-brightness-control), mapping
|
||||
every request through the one curve; installed-fresh appends the define last, adds the boot-args, and refuses until the
|
||||
under ADRENO_BACKLIGHT, publishes what BrightnessControl reads to bind the panel's IOMFB (not
|
||||
force-brightness-control), takes the brightness from the swap record and maps every request through the one curve;
|
||||
installed-fresh appends the define last, adds the boot-args, and refuses until the
|
||||
AuxKC is pinned.
|
||||
|
||||
UserspaceContract (a 26A428 host with ipsw only, else SKIP): the dyld cache's BrightnessControl and IOMobileFramebuffer
|
||||
still carry the names that path relies on (gpu/adreno-kext/kext/src/AdrenoBacklight.h).
|
||||
UserspaceContract (a 26A428 host with ipsw only, else SKIP): the dyld cache's BrightnessControl, IOMobileFramebuffer and
|
||||
QuartzCore still carry the names that path relies on, and IOMobileFramebufferSwapSetBrightness still writes the swap
|
||||
record where the kext reads it (gpu/adreno-kext/kext/src/AdrenoBacklight.h).
|
||||
|
||||
--build: xnuboot with each --qualification's flags, define absent, =0 and =1: absent == =0 == the qualification's
|
||||
monitor_sha256; =1 differs and fits the code budget (probes/run.py OUT_AT - CODE_AT), its headroom printed.
|
||||
@@ -58,6 +60,13 @@ MUTATIONS = (
|
||||
'if (nits_q16 >= full) return max - 1;'),
|
||||
('minimum below the floor', 'compat/edp-backlight.h', 'return configured > at_floor ? configured : at_floor;',
|
||||
'return configured + 0 * at_floor;'),
|
||||
('swap NaN accepted', 'compat/edp-backlight.h', 'if (exponent == 0x7ffu || (bits >> 63 && bits << 1)) return -1;',
|
||||
'if (bits >> 63 && bits << 1) return -1;'),
|
||||
('swap negative accepted', 'compat/edp-backlight.h',
|
||||
'if (exponent == 0x7ffu || (bits >> 63 && bits << 1)) return -1;', 'if (exponent == 0x7ffu) return -1;'),
|
||||
('swap nits truncated', 'compat/edp-backlight.h',
|
||||
'v = (mantissa + (1ull << (1059 - exponent - 1))) >> (1059 - exponent);', 'v = mantissa >> (1059 - exponent);'),
|
||||
('swap cap ignored', 'compat/edp-backlight.h', '*q16 = v > cap_q16 ? cap_q16 : v;', '*q16 = v + 0 * cap_q16;'),
|
||||
('no clamp', 'compat/edp-backlight.c', 'uint32_t level = edp_bl_clamp(request, s->floor, s->max);',
|
||||
'uint32_t level = (uint32_t)request;'),
|
||||
('mask bits dropped', 'compat/edp-backlight.c', ' << 1 | (status & EDP_INTR_MASK_BITS));', ' << 1);'),
|
||||
@@ -175,19 +184,33 @@ class Wiring(unittest.TestCase):
|
||||
self.assertEqual(bad, [])
|
||||
|
||||
def test_kext_brightness_path(self):
|
||||
# The service carries what WindowServer's BrightnessControl needs to write brightness-nits to it
|
||||
# (gpu/adreno-kext/kext/src/AdrenoBacklight.h, steps 2 and 3), and every request goes through the curve.
|
||||
src = (ROOT / 'gpu/adreno-kext/kext/src/AdrenoDisplay/backlight.cpp').read_text()
|
||||
force = re.search(r'kForceBrightnessControl\[(\d+)\] = \{([^}]*)\}', src)
|
||||
self.assertIsNotNone(force, 'force-brightness-control bytes not found')
|
||||
self.assertNotEqual(int(force.group(2).split(',')[0], 0), 0, 'its first byte must be nonzero')
|
||||
for needle in ('setProperty("force-brightness-control", (void *)kForceBrightnessControl,',
|
||||
'setProperty("backlight-control", kOSBooleanTrue)', 'd->getObject("brightness-nits")',
|
||||
'putDict(params, "BrightnessMilliNits", nits)', 'setProperty("IODisplayParameters", params)',
|
||||
# The service carries what WindowServer's BrightnessControl reads to bind the panel's IOMFB (and nothing that
|
||||
# makes it write to the service: the sandbox refuses that), AdrenoDisplay takes the brightness from the swap
|
||||
# record, and every request goes through the one curve (gpu/adreno-kext/kext/src/AdrenoBacklight.h).
|
||||
kext = ROOT / 'gpu/adreno-kext/kext/src'
|
||||
src = (kext / 'AdrenoDisplay/backlight.cpp').read_text()
|
||||
for needle in ('setProperty("backlight-control", kOSBooleanTrue)', 'putDict(params, "BrightnessMilliNits", nits)',
|
||||
'setProperty("IODisplayParameters", params)',
|
||||
'edp_bl_nits_to_level(nits_q16 < fMinQ16 ? fMinQ16 : nits_q16, fMaxNits, fFloor, fMax)',
|
||||
'fMinQ16 = edp_bl_min_nits_q16(fMinNits, fMaxNits, fFloor, fMax);'):
|
||||
'fMinQ16 = edp_bl_min_nits_q16(fMinNits, fMaxNits, fFloor, fMax);',
|
||||
'uint64_t AdrenoDisplay::swap_submit(IOMFBSwapRec *rec, IOUserClient *client, unsigned int *swapID)',
|
||||
'if (!edp_bl_double_to_q16(bits, (uint64_t)EDP_BL_NITS_LIMIT_MAX << 16, &q16))',
|
||||
'return IOMobileFramebufferLegacy::swap_submit(rec, client, swapID);',
|
||||
'fBacklight->requestNits(v & ~kSwapNitsPending, AdrenoBacklight::kSourceSwap);'):
|
||||
self.assertIn(needle, src)
|
||||
self.assertNotIn('force-brightness-control', src)
|
||||
self.assertEqual(src.count('edp_bl_nits_to_level('), 1, 'one mapping for every source')
|
||||
# The record's brightness lies where IOMobileFramebufferSwapSetBrightness writes it (UserspaceContract checks
|
||||
# the same constants against the dyld cache).
|
||||
consts = dict(re.findall(r'static constexpr size_t (kSwap\w+) = (0x[0-9a-f]+);', src))
|
||||
self.assertEqual({k: int(v, 16) for k, v in consts.items()},
|
||||
{'kSwapRecordSize': 0x6e0, 'kSwapBrightnessPresent': 0x354, 'kSwapBrightnessNits': 0x35e})
|
||||
header = (kext / 'AdrenoDisplay.h').read_text()
|
||||
self.assertIn('virtual uint64_t swap_submit(IOMFBSwapRec *rec, IOUserClient *client, unsigned int *swapID) '
|
||||
'APPLE_KEXT_OVERRIDE;', header)
|
||||
bad = scope_violations(kext / 'AdrenoDisplay/presentation.cpp', r'backlight', 'ADRENO_BACKLIGHT')
|
||||
self.assertEqual(bad, [])
|
||||
self.assertIn('backlightSwapGated();', (kext / 'AdrenoDisplay/presentation.cpp').read_text())
|
||||
|
||||
def test_cycle(self):
|
||||
from cycle_lib import installed_fresh as f
|
||||
@@ -231,14 +254,21 @@ class Wiring(unittest.TestCase):
|
||||
|
||||
DSC = Path('/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e')
|
||||
IPSW = shutil.which('ipsw') or '/opt/homebrew/bin/ipsw'
|
||||
# The names the guest's 26A428 userspace must still use for AdrenoBacklight.h's path, per image: BrightnessControl's
|
||||
# match key, the keys it reads and writes, and the BrightnessControl entry points IOMobileFramebuffer resolves.
|
||||
# What the guest's 26A428 userspace must still do for AdrenoBacklight.h's path. Strings per image: BrightnessControl's
|
||||
# match key and the keys it reads; the BrightnessControl entry points IOMobileFramebuffer resolves to bind an IOMFB.
|
||||
USERSPACE_NAMES = {
|
||||
'BrightnessControl': ('IOPropertyMatch', 'backlight-control', 'force-brightness-control', 'brightness-nits',
|
||||
'IODisplayParameters', 'BrightnessMilliNits', 'LmaxProduct'),
|
||||
'IOMobileFramebuffer': ('BCBrtControlNewMonitorForAllDisplaysWithHandler', 'BCBrtControlGetCapabilities',
|
||||
'BCBrtControlSetNits'),
|
||||
'BrightnessControl': ('IOPropertyMatch', 'backlight-control', 'IODisplayParameters', 'BrightnessMilliNits',
|
||||
'LmaxProduct'),
|
||||
'IOMobileFramebuffer': ('BCBrtControlNewMonitorForAllDisplaysWithHandler', 'BCBrtControlGetCapabilities'),
|
||||
}
|
||||
# Symbols per image: the swap brightness call and CoreAnimation's per-swap sender.
|
||||
USERSPACE_SYMBOLS = {
|
||||
'IOMobileFramebuffer': ('_IOMobileFramebufferSwapSetBrightness',),
|
||||
'QuartzCore': ('__ZN2CA12WindowServer12IOMFBDisplay15swap_brightnessEv',),
|
||||
}
|
||||
# The framework keeps the swap record at this offset of its framebuffer object (kern_SwapBegin clears it there and
|
||||
# kern_SwapEnd submits it from there as external method 5).
|
||||
RECORD_BASE = 0x18
|
||||
|
||||
|
||||
def host_build():
|
||||
@@ -263,6 +293,32 @@ class UserspaceContract(unittest.TestCase):
|
||||
missing = [(image, n) for image, ns in USERSPACE_NAMES.items() for n in ns if (n, image) not in found]
|
||||
self.assertEqual(missing, [])
|
||||
|
||||
def test_symbols_present(self):
|
||||
for image, symbols in USERSPACE_SYMBOLS.items():
|
||||
for symbol in symbols:
|
||||
out = subprocess.run([IPSW, 'dsc', 'symaddr', str(DSC), symbol, '--image', image, '--no-color'],
|
||||
capture_output=True, text=True, timeout=600)
|
||||
self.assertRegex(out.stdout, r'0x[0-9a-f]+:.*\b%s\s+%s\b' % (re.escape(symbol), image),
|
||||
f'{image}: {symbol}')
|
||||
|
||||
def disass(self, symbol):
|
||||
out = subprocess.run([IPSW, 'dsc', 'disass', str(DSC), '--symbol', symbol, '--symbol-image',
|
||||
'IOMobileFramebuffer', '--no-color'], capture_output=True, text=True, timeout=600)
|
||||
self.assertEqual(out.returncode, 0, out.stderr[-1000:])
|
||||
return out.stdout
|
||||
|
||||
def test_record_offsets(self):
|
||||
# The kext's offsets, rebased onto the framework's object: the setter must address both, and the submit must
|
||||
# hand the whole record (that size, from that base) to external method 5.
|
||||
src = (ROOT / 'gpu/adreno-kext/kext/src/AdrenoDisplay/backlight.cpp').read_text()
|
||||
consts = {k: int(v, 16) for k, v in re.findall(r'static constexpr size_t (kSwap\w+) = (0x[0-9a-f]+);', src)}
|
||||
setter = self.disass('_kern_SwapSetBrightness')
|
||||
for field in ('kSwapBrightnessPresent', 'kSwapBrightnessNits'):
|
||||
self.assertRegex(setter, r'#%#x\b' % (RECORD_BASE + consts[field]), field)
|
||||
submit = self.disass('_kern_SwapEnd')
|
||||
for value in (RECORD_BASE, consts['kSwapRecordSize'], 5):
|
||||
self.assertRegex(submit, r'#%#x\b' % value)
|
||||
|
||||
|
||||
@unittest.skipUnless(ARGS.build, 'pass --build to compile the monitor (minutes)')
|
||||
class Build(unittest.TestCase):
|
||||
@@ -302,14 +358,16 @@ class Kext(unittest.TestCase):
|
||||
c = subprocess.run([sys.executable, str(ROOT / 'tools/macho-sections.py'), str(ref),
|
||||
str(out / 'default/AdrenoX2.kext/Contents/MacOS/AdrenoX2')], capture_output=True, text=True)
|
||||
self.assertRegex(c.stdout, r'(\d+) of \1 sections identical', c.stdout)
|
||||
default = (out / 'default/AdrenoX2.kext/Contents/MacOS/AdrenoX2').read_bytes()
|
||||
self.assertNotIn(b'force-brightness-control', default)
|
||||
default = subprocess.run(['nm', '-m', str(out / 'default/AdrenoX2.kext/Contents/MacOS/AdrenoX2')],
|
||||
capture_output=True, text=True).stdout
|
||||
self.assertNotIn('__ZN13AdrenoDisplay11swap_submitEP12IOMFBSwapRecP12IOUserClientPj', default)
|
||||
for profile in profiles:
|
||||
binary = out / profile / 'AdrenoX2.kext/Contents/MacOS/AdrenoX2'
|
||||
nm = subprocess.run(['nm', '-m', str(binary)], capture_output=True, text=True).stdout
|
||||
self.assertIn('__ZN13AdrenoDisplay20set_brightness_gatedEi', nm, profile)
|
||||
self.assertIn('__ZN13AdrenoDisplay11swap_submitEP12IOMFBSwapRecP12IOUserClientPj', nm, profile)
|
||||
self.assertIn('__ZN15AdrenoBacklight10gMetaClassE', nm, profile)
|
||||
self.assertIn(b'force-brightness-control\0', binary.read_bytes(), profile)
|
||||
self.assertNotIn(b'force-brightness-control', binary.read_bytes(), profile)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in new issue
Block a user