mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Add opt-in D3D9 single-sample workaround and fix HUD texel format
This commit is contained in:
1 parent
2204da404a
commit
88dfddd18a
7 files changed
+461
No files matched your search
@@ -0,0 +1,108 @@
|
||||
# American Nightmare on Pixel: rendering workaround and remaining limits
|
||||
|
||||
Local engineering results from 2026-10-11/12. Steam game 202750 was tested on a
|
||||
Pixel 11 Pro Fold using the Pixel Lab runtime. These observations do not establish
|
||||
compatibility across the Pixel family or a fix for the system resets.
|
||||
|
||||
## Rendering result
|
||||
|
||||
The bright dotted triangle edges over textured rocks, boards and the player are
|
||||
not an application wireframe setting: traced input and driver pipelines all used
|
||||
`VK_POLYGON_MODE_FILL`, with no dynamic polygon mode. The original affected path
|
||||
used 2x MSAA, RGBA16F color and D24S8 depth. The game's minimum UI AA setting and
|
||||
`d3d11.disableMsaa` did not disable its D3D9 multisampling.
|
||||
|
||||
A real single-sample D3D9 comparison removed the visible bright triangle edges.
|
||||
Trial 16 verified 114 input and 114 driver pipelines, all single-sample. Trial 17
|
||||
retained correct meshes/textures through movement and turning, with 119 pipelines
|
||||
in each phase, all single-sample. This isolates a useful workaround to the MSAA
|
||||
path; it does not identify the precise faulty operation or prove a vendor bug.
|
||||
The override also changes effective surface descriptors, so the application may
|
||||
select different AA-aware shader/rendering paths.
|
||||
|
||||
`tools/dxvk/patches/0004-d3d9-force-single-sample.patch` adds an explicit per-game
|
||||
`d3d9.forceSingleSample` option, default **False**. After validating the original
|
||||
sample request, D3D9 surface normalization selects `D3DMULTISAMPLE_NONE` and quality
|
||||
0. GetDesc, image creation, resolves and sample-dependent state use this effective
|
||||
description. This intentionally disables MSAA, per-sample masks and
|
||||
alpha-to-coverage. It is a compatibility override, not transparent MSAA emulation
|
||||
or a universal PowerVR default. Existing low32, BC-format, present synchronization
|
||||
and tiler workarounds must remain intact.
|
||||
|
||||
## Independent HUD correction
|
||||
|
||||
The diagnostic frametimes HUD bound an R8_UINT storage texel buffer to a shader
|
||||
whose SPIR-V image format was Unknown. This required formatless buffer writes
|
||||
which the device did not report for that format, producing VUID 07029.
|
||||
`0003-hud-explicit-r8-texel-format.patch` declares `r8ui` to match the actual host
|
||||
view. No capabilities are fabricated. The build now compiles and validates the
|
||||
actual shader, and checks binding 3 for DimBuffer/storage/R8ui. The original shader
|
||||
fails that contract; the patched shader passes. Real-game trial 15 used the new
|
||||
DLLs, removed 07029, and retained the original triangle edges. This HUD bug is
|
||||
separate from the rendering workaround, and is not a proven reset cause.
|
||||
|
||||
VUID 06479 for color-format comparison descriptors remains under investigation.
|
||||
The D3D9 shader uses a conditional comparison path; static validation output alone
|
||||
does not prove that the offending branch executes. No warning suppression or
|
||||
false format-feature claims are proposed. Trial 14's LOG+FAIL with a duplicate
|
||||
limit blocked only the first message of each kind, not every invalid API call;
|
||||
subsequent trials used logging only. Do not describe these runs as fully blocked
|
||||
validation or GPU-assisted validation.
|
||||
|
||||
## Responsiveness, performance and stability
|
||||
|
||||
Paired screenshots prove movement and camera changes, rather than relying on an
|
||||
input receipt. Trial 17 completed eight samples without a black viewport. Its
|
||||
cold sample was 3.2 FPS; later recognized samples were 19.1-29.9 FPS, with roughly
|
||||
2.54 CPU-core equivalents. Camera/workload, capture overhead and thermals were
|
||||
not controlled; this is not a reliable A/B benchmark.
|
||||
|
||||
The planned eight-minute trial 17 stopped at a focus/session guard after the last
|
||||
confirmed sample at 315.6 seconds. Steam owned focus; the target game was still
|
||||
available for a single client quit. Its temporary settings were successfully
|
||||
restored, and the phone boot counter remained 12. The focus change's cause is
|
||||
unknown. The run is **incomplete**, not an eight-minute pass or a demonstrated
|
||||
game crash. Controls, focus ownership and sustained stability still need broader
|
||||
coverage beyond these short tests.
|
||||
|
||||
## Native guards, resets and diagnostic controls
|
||||
|
||||
The Pixel base is e589e3ae21959c9c74c490d917cebf256acc8ac8. PR561's 9e6fe314 and
|
||||
3c363910 guards were integrated in an isolated worktree; 26 selected production-C
|
||||
regressions and a full APK/runtime verification passed. Pixel AHB support remains
|
||||
present. The existing Lab app was updated without uninstalling or resetting its
|
||||
Steam account. This APK preceded a second system reset, so the native guards are
|
||||
not a verified reset fix.
|
||||
|
||||
Two resets changed the boot counter 10 -> 11 -> 12. After personal unlock, narrow
|
||||
kernel/firmware projections confirmed GNOC/access-control details for the first,
|
||||
and `reset,warm,gnoc_error` with a fresh GNOC numeric record for the second. The
|
||||
specific faulty component/operation and any relation to rendering are unknown.
|
||||
`tools/pixel_reset_diagnostics.py` exports only fixed fault categories/numeric
|
||||
fields, lock state, boot counter and entry times; never arbitrary log text or
|
||||
account data. Seven host tests cover its gating and narrow output.
|
||||
|
||||
Private opt-in diagnostics captured maintenance5 inline modules, which the older
|
||||
shim path missed. Materialization passed nine actual-C ASan/UBSan cases; benign
|
||||
pipeline-chain planning passed twelve. Real-game NoContraction, scaled vertex
|
||||
conversion and force-clip rewrites were actually applied and did not remove the
|
||||
edges. None is a production default. Sixty-two valid bounded, owned offscreen GPU
|
||||
cases passed exact pixel readback, fences and validation, covering interpolation,
|
||||
seams, perspective meshes, interface widening, scaled fetch, HDR/depth prepasses,
|
||||
real clipping and nonconstant depth. Invalid initial fixtures are excluded. These
|
||||
passing controls do not exonerate every driver/rendering path.
|
||||
|
||||
## Reproducibility and recovery
|
||||
|
||||
The runtime artifact supplies both architectures, exact DLL hashes, public patched
|
||||
DXVK sources, all four patches, wrapper and build recipe. It contains no game
|
||||
assets, bytecode, saves, account files, screenshots or raw device logs. Source
|
||||
patches apply cleanly to pinned DXVK revision 25ca63f17f34bdc05a39873ee63907d3cbbfa030.
|
||||
All five runtime DLL targets were built for both architectures.
|
||||
|
||||
Stop further load tests after a full reset. Wait for personal unlock and inspect a
|
||||
fresh narrow projection before another launch. Temporary profile restoration checks
|
||||
baseline/current hashes and unchanged shared/other-game settings; it never rolls
|
||||
back saved game progress. Persistent selection must affect only 202750 and leave
|
||||
private shader audit layers, validation settings and frametimes HUD disabled.
|
||||
Routine Heartbeat must not launch games or automatically resume this engineering.
|
||||
@@ -17,6 +17,7 @@ for patch in "$root"/tools/dxvk/patches/*.patch; do
|
||||
git -C "$work/source" apply "$patch"
|
||||
done
|
||||
python3 "$root/tools/dxvk/check_present_sync.py" "$work/source/src/dxvk/dxvk_presenter.cpp"
|
||||
python3 "$root/tools/dxvk/check_hud_storage_format.py" "$work/source/src/dxvk/hud/shaders/hud_frame_time_eval.comp"
|
||||
for arch in 32 64; do
|
||||
meson setup "$work/build$arch" "$work/source" --cross-file "$work/source/build-win$arch.txt" \
|
||||
--buildtype release --prefix "$out" --bindir "x$arch" --libdir "x$arch" -Db_ndebug=if-release --strip
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile the actual HUD shader and verify its binding-3 R8_UINT contract.
|
||||
|
||||
The frame-time HUD binds an R8_UINT texel-buffer view. An Unknown SPIR-V image
|
||||
format requires STORAGE_WRITE_WITHOUT_FORMAT, which PowerVR does not report
|
||||
for that buffer format. Static spirv-val alone accepts both declarations;
|
||||
this check validates the shader against the known host-side binding contract.
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
def check(shader):
|
||||
compiler = shutil.which('glslangValidator')
|
||||
validator = shutil.which('spirv-val')
|
||||
if not compiler or not validator:
|
||||
raise RuntimeError('glslangValidator and spirv-val required')
|
||||
with tempfile.TemporaryDirectory(prefix='dxvk-hud-contract-') as directory:
|
||||
output = Path(directory) / 'hud.spv'
|
||||
for command in ([compiler, '-V', '--target-env', 'vulkan1.3', str(shader), '-o', str(output)],
|
||||
[validator, '--target-env', 'vulkan1.3', str(output)]):
|
||||
process = subprocess.run(command, capture_output=True, text=True, timeout=30)
|
||||
if process.returncode:
|
||||
raise RuntimeError('HUD shader compilation/SPIR-V validation failed')
|
||||
raw = output.read_bytes()
|
||||
if len(raw) < 20 or len(raw) % 4:
|
||||
raise RuntimeError('Malformed SPIR-V')
|
||||
words = struct.unpack('<' + str(len(raw) // 4) + 'I', raw)
|
||||
if words[0] != 0x07230203:
|
||||
raise RuntimeError('Unexpected SPIR-V magic')
|
||||
bindings, sets, variables, pointers, images = {}, {}, {}, {}, {}
|
||||
offset = 5
|
||||
while offset < len(words):
|
||||
size, opcode = words[offset] >> 16, words[offset] & 0xffff
|
||||
if not size or size > len(words) - offset:
|
||||
raise RuntimeError('Malformed SPIR-V instruction')
|
||||
instruction = words[offset:offset + size]
|
||||
if opcode == 71 and size == 4: # OpDecorate
|
||||
if instruction[2] == 33:
|
||||
bindings[instruction[1]] = instruction[3]
|
||||
elif instruction[2] == 34:
|
||||
sets[instruction[1]] = instruction[3]
|
||||
elif opcode == 59 and size >= 4: # OpVariable
|
||||
variables[instruction[2]] = instruction[1]
|
||||
elif opcode == 32 and size == 4: # OpTypePointer
|
||||
pointers[instruction[1]] = instruction[3]
|
||||
elif opcode == 25 and size >= 9: # OpTypeImage
|
||||
images[instruction[1]] = (instruction[3], instruction[7], instruction[8])
|
||||
offset += size
|
||||
targets = [images.get(pointers.get(variables.get(identifier)))
|
||||
for identifier, binding in bindings.items()
|
||||
if binding == 3 and sets.get(identifier) == 0]
|
||||
if targets != [(5, 2, 39)]: # DimBuffer, storage image, ImageFormatR8ui
|
||||
raise RuntimeError('HUD binding 3 must compile to a storage texel buffer with ImageFormatR8ui')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('shader', type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
check(args.shader)
|
||||
except RuntimeError as error:
|
||||
parser.exit(1, str(error) + '\n')
|
||||
print('HUD binding 3: SPIR-V valid, explicit R8ui storage texel-buffer format')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
--- a/src/dxvk/hud/shaders/hud_frame_time_eval.comp
|
||||
+++ b/src/dxvk/hud/shaders/hud_frame_time_eval.comp
|
||||
@@ -41,7 +41,8 @@
|
||||
draw_info_t draw_infos[];
|
||||
};
|
||||
|
||||
-layout(binding = 3)
|
||||
+// The bound text view is VK_FORMAT_R8_UINT; do not require formatless writes.
|
||||
+layout(binding = 3, r8ui)
|
||||
uniform writeonly uimageBuffer text_buffer;
|
||||
|
||||
layout(push_constant)
|
||||
@@ -0,0 +1,66 @@
|
||||
diff --git a/dxvk.conf b/dxvk.conf
|
||||
index ba4bc20..26d3127 100644
|
||||
--- a/dxvk.conf
|
||||
+++ b/dxvk.conf
|
||||
@@ -336,6 +336,15 @@
|
||||
|
||||
# d3d11.disableMsaa = False
|
||||
|
||||
+# Forces D3D9 surfaces to single-sample after validating the original request.
|
||||
+# The effective surface description reports D3DMULTISAMPLE_NONE; MSAA quality,
|
||||
+# alpha-to-coverage and per-sample masks are consequently disabled. This is an
|
||||
+# opt-in compatibility override for games with broken MSAA rendering.
|
||||
+#
|
||||
+# Supported values: True, False
|
||||
+
|
||||
+# d3d9.forceSingleSample = False
|
||||
+
|
||||
|
||||
# Forces insertion of memory and control barriers after writes to group-shared
|
||||
# memory in compute shaders. This is only intended to be used as a workaround
|
||||
diff --git a/src/d3d9/d3d9_common_texture.cpp b/src/d3d9/d3d9_common_texture.cpp
|
||||
index 9d5fe38..629fea3 100644
|
||||
--- a/src/d3d9/d3d9_common_texture.cpp
|
||||
+++ b/src/d3d9/d3d9_common_texture.cpp
|
||||
@@ -212,6 +212,15 @@ namespace dxvk {
|
||||
return D3DERR_INVALIDCALL;
|
||||
}
|
||||
|
||||
+ // Keep original request validation above. This opt-in override changes
|
||||
+ // the effective surface description as well as the backing image.
|
||||
+ // All surface descriptors and resolves then see the actual sample count.
|
||||
+ if (ResourceType == D3DRTYPE_SURFACE && options->forceSingleSample
|
||||
+ && sampleCount != VK_SAMPLE_COUNT_1_BIT) {
|
||||
+ pDesc->MultiSample = D3DMULTISAMPLE_NONE;
|
||||
+ pDesc->MultisampleQuality = 0;
|
||||
+ }
|
||||
+
|
||||
// Using MANAGED pool with DYNAMIC usage is illegal
|
||||
if (IsPoolManaged(pDesc->Pool) && (pDesc->Usage & D3DUSAGE_DYNAMIC))
|
||||
return D3DERR_INVALIDCALL;
|
||||
diff --git a/src/d3d9/d3d9_options.cpp b/src/d3d9/d3d9_options.cpp
|
||||
index e46864e..0cab7dc 100644
|
||||
--- a/src/d3d9/d3d9_options.cpp
|
||||
+++ b/src/d3d9/d3d9_options.cpp
|
||||
@@ -59,6 +59,7 @@ namespace dxvk {
|
||||
this->memoryTrackTest = config.getOption<bool> ("d3d9.memoryTrackTest", false);
|
||||
this->forceSamplerTypeSpecConstants = config.getOption<bool> ("d3d9.forceSamplerTypeSpecConstants", false);
|
||||
this->forceSampleRateShading = config.getOption<bool> ("d3d9.forceSampleRateShading", false);
|
||||
+ this->forceSingleSample = config.getOption<bool> ("d3d9.forceSingleSample", false);
|
||||
this->forceAspectRatio = config.getOption<std::string> ("d3d9.forceAspectRatio", "");
|
||||
this->forceRefreshRate = config.getOption<int32_t> ("d3d9.forceRefreshRate", 0u);
|
||||
this->modeCountCompatibility = config.getOption<bool> ("d3d9.modeCountCompatibility", false);
|
||||
diff --git a/src/d3d9/d3d9_options.h b/src/d3d9/d3d9_options.h
|
||||
index e69a39a..4478420 100644
|
||||
--- a/src/d3d9/d3d9_options.h
|
||||
+++ b/src/d3d9/d3d9_options.h
|
||||
@@ -113,6 +113,9 @@ namespace dxvk {
|
||||
/// Forces sample rate shading
|
||||
bool forceSampleRateShading;
|
||||
|
||||
+ /// Optional compatibility override: report and render surfaces as single-sample.
|
||||
+ bool forceSingleSample;
|
||||
+
|
||||
/// Allow D3DLOCK_DISCARD
|
||||
bool allowDiscard;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only reboot projection. Never writes raw Android logs or account data.
|
||||
|
||||
This is evidence collection, not a GPU fault injector or a root-cause oracle.
|
||||
The optional kernel projection reads only SYSTEM_LAST_KMSG after user unlock.
|
||||
It emits fixed categories and numeric hardware fields, never arbitrary log lines.
|
||||
"""
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
ENTRY = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(SYSTEM_LAST_KMSG)\b', re.M)
|
||||
CATEGORIES = ('GNOC', 'PVRSRV', 'watchdog', 'IOMMU', 'S2MPU', 'access control', 'Kernel panic', 'HWR', 'GPU fault')
|
||||
|
||||
|
||||
def primary_state(text):
|
||||
match = re.search(r'Started users state: \[0=(RUNNING_[A-Z_]+)', text)
|
||||
return match.group(1) if match else 'unknown'
|
||||
|
||||
|
||||
def safe_reason(text):
|
||||
value = text.strip()
|
||||
return value if re.fullmatch(r'[A-Za-z0-9_, .():-]{1,160}', value) else 'unknown'
|
||||
|
||||
|
||||
def kernel_projection(text, device_timezone, after=None):
|
||||
entries = list(ENTRY.finditer(text))
|
||||
result = {'metadata_dates': [m.group(1) for m in entries], 'raw_body_saved': False,
|
||||
'root_cause_proven': False}
|
||||
if not entries:
|
||||
result['status'] = 'no_accessible_entry'
|
||||
return result
|
||||
latest = max(enumerate(entries), key=lambda row: row[1].group(1))
|
||||
index, match = latest
|
||||
body = text[match.end():entries[index + 1].start() if index + 1 < len(entries) else len(text)]
|
||||
result['entry_date_device_local'] = match.group(1)
|
||||
try:
|
||||
local = datetime.strptime(match.group(1), '%Y-%m-%d %H:%M:%S').replace(tzinfo=ZoneInfo(device_timezone))
|
||||
result['entry_date_utc'] = local.astimezone(timezone.utc).isoformat()
|
||||
except (ValueError, ZoneInfoNotFoundError):
|
||||
result['status'] = 'unknown_device_timezone'
|
||||
return result
|
||||
if after is not None and local < after:
|
||||
result['status'] = 'stale_entry_not_attributed_to_current_trial'
|
||||
return result
|
||||
result['status'] = 'projected_entry_not_a_root_cause'
|
||||
result['categories'] = {key: len(re.findall(re.escape(key), body, re.I)) for key in CATEGORIES}
|
||||
patterns = {
|
||||
'gnoc_addresses': r'Error from GNOC at addr (0x[0-9a-fA-F]+)',
|
||||
'gnoc_error_types': r'GNOC error type (0x[0-9a-fA-F]+)',
|
||||
'firmware_reboot_codes': r'Reboot reason:\s+(0x[0-9a-fA-F]+)',
|
||||
'access_control_addresses': r'Access Control Error \((0x[0-9a-fA-F]+)',
|
||||
}
|
||||
result['hardware_fields'] = {key: sorted(set(re.findall(pattern, body))) for key, pattern in patterns.items()}
|
||||
reasons = re.findall(r'Last boot reason:\s*([A-Za-z0-9_,.-]+)', body)
|
||||
result['last_boot_reasons'] = sorted(set(reasons))
|
||||
return result
|
||||
|
||||
|
||||
def collect(adb, serial, include_kernel=False, after=None):
|
||||
def shell(*args):
|
||||
try:
|
||||
process = subprocess.run([adb, '-s', serial, 'shell', *args], capture_output=True,
|
||||
text=True, timeout=20)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None, ''
|
||||
# No command stderr is copied to output: it may contain unrelated data.
|
||||
return process.returncode, process.stdout if process.returncode == 0 else ''
|
||||
|
||||
result = {'at': datetime.now(timezone.utc).isoformat(), 'read_only': True,
|
||||
'raw_logs_saved': False, 'credentials_collected': False}
|
||||
code, value = shell('settings', 'get', 'global', 'boot_count')
|
||||
result['boot_count'] = int(value.strip()) if code == 0 and value.strip().isdigit() else None
|
||||
code, value = shell('getprop', 'ro.boot.bootreason')
|
||||
result['bootloader_reason'] = safe_reason(value)
|
||||
code, value = shell('getprop', 'sys.boot.reason')
|
||||
result['system_boot_reason'] = safe_reason(value)
|
||||
code, value = shell('dumpsys', 'user')
|
||||
result['primary_user_state'] = primary_state(value)
|
||||
code, value = shell('getprop', 'persist.sys.timezone')
|
||||
device_timezone = value.strip()
|
||||
try:
|
||||
ZoneInfo(device_timezone)
|
||||
result['device_timezone'] = device_timezone
|
||||
except (ValueError, ZoneInfoNotFoundError):
|
||||
result['device_timezone'] = 'unknown'
|
||||
code, value = shell('dumpsys', 'dropbox', 'SYSTEM_LAST_KMSG')
|
||||
result['kernel_metadata_dates'] = [m.group(1) for m in ENTRY.finditer(value)]
|
||||
if include_kernel:
|
||||
if result['primary_user_state'] != 'RUNNING_UNLOCKED':
|
||||
result['kernel'] = {'status': 'requires_user_unlock', 'raw_body_saved': False}
|
||||
else:
|
||||
code, value = shell('dumpsys', 'dropbox', '--print', 'SYSTEM_LAST_KMSG')
|
||||
result['kernel'] = kernel_projection(value, result['device_timezone'], after)
|
||||
result['kernel']['read_exit_code'] = code
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--adb', default='adb')
|
||||
parser.add_argument('--serial', required=True)
|
||||
parser.add_argument('--kernel', action='store_true')
|
||||
parser.add_argument('--after', help='Earliest relevant UTC ISO timestamp; excludes stale kernel entries')
|
||||
parser.add_argument('--output', type=Path, help='New JSON file; refuses to overwrite')
|
||||
args = parser.parse_args()
|
||||
after = datetime.fromisoformat(args.after.replace('Z', '+00:00')) if args.after else None
|
||||
if after is not None and after.tzinfo is None:
|
||||
parser.error('--after must include a timezone')
|
||||
result = collect(args.adb, args.serial, args.kernel, after)
|
||||
output = json.dumps(result, indent=2) + '\n'
|
||||
if args.output:
|
||||
with args.output.open('x') as stream:
|
||||
stream.write(output)
|
||||
args.output.chmod(0o600)
|
||||
print(output, end='')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Test freshness, narrow output, and lock handling without accessing a device."""
|
||||
from datetime import datetime, timezone
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location('reset_projection', Path(__file__).resolve().parents[1] / 'pixel_reset_diagnostics.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
class ResetProjectionTest(unittest.TestCase):
|
||||
def test_only_primary_state(self):
|
||||
self.assertEqual(module.primary_state('Private space secret\nStarted users state: [0=RUNNING_UNLOCKED, 10=STOPPED]'), 'RUNNING_UNLOCKED')
|
||||
self.assertEqual(module.primary_state('State: RUNNING_UNLOCKED'), 'unknown')
|
||||
|
||||
def test_stale_body_not_used(self):
|
||||
raw = '2026-10-10 20:10:09 SYSTEM_LAST_KMSG (text, 1024 bytes)\nGNOC error type 0x2000'
|
||||
result = module.kernel_projection(raw, 'America/Toronto', datetime(2026, 10, 11, 21, tzinfo=timezone.utc))
|
||||
self.assertEqual(result['status'], 'stale_entry_not_attributed_to_current_trial')
|
||||
self.assertNotIn('categories', result)
|
||||
|
||||
def test_latest_entry_and_no_arbitrary_body(self):
|
||||
raw = ('2026-10-10 20:10:09 SYSTEM_LAST_KMSG (text)\nGNOC error type 0x1111\n'
|
||||
'2026-10-11 17:35:01 SYSTEM_LAST_KMSG (text)\n'
|
||||
'PRIVATE_TEST_VALUE=a_secret_that_must_not_be_emitted\n'
|
||||
'Error from GNOC at addr 0x28220000\nGNOC error type 0x2000\n'
|
||||
'Reboot reason: 0xc005 - Secret arbitrary description\nLast boot reason: reset,warm,gnoc_error\n')
|
||||
result = module.kernel_projection(raw, 'America/Toronto', datetime(2026, 10, 11, 21, tzinfo=timezone.utc))
|
||||
self.assertEqual(result['entry_date_utc'], '2026-10-11T21:35:01+00:00')
|
||||
self.assertEqual(result['hardware_fields']['gnoc_error_types'], ['0x2000'])
|
||||
self.assertNotIn('0x1111', json.dumps(result))
|
||||
self.assertNotIn('a_secret', json.dumps(result))
|
||||
self.assertNotIn('arbitrary description', json.dumps(result))
|
||||
self.assertFalse(result['root_cause_proven'])
|
||||
|
||||
def test_missing_entry_and_timezone(self):
|
||||
self.assertEqual(module.kernel_projection('Permission denied', 'America/Toronto')['status'], 'no_accessible_entry')
|
||||
self.assertEqual(module.kernel_projection('2026-10-11 17:00:00 SYSTEM_LAST_KMSG\nGNOC', 'Unknown/Region')['status'], 'unknown_device_timezone')
|
||||
|
||||
def test_reason_rejects_paths_and_urls(self):
|
||||
self.assertEqual(module.safe_reason('reset,warm,gnoc_error\n'), 'reset,warm,gnoc_error')
|
||||
self.assertEqual(module.safe_reason('https://private.invalid/login?secret=abc'), 'unknown')
|
||||
self.assertEqual(module.safe_reason('/data/private'), 'unknown')
|
||||
|
||||
def test_locked_skips_kernel_body(self):
|
||||
commands = []
|
||||
def run(argv, **kwargs):
|
||||
commands.append(argv)
|
||||
if argv[-2:] == ['dumpsys', 'user']:
|
||||
text = 'Started users state: [0=RUNNING_LOCKED]'
|
||||
elif argv[-1] == 'persist.sys.timezone':
|
||||
text = 'America/Toronto'
|
||||
elif argv[-3:] == ['get', 'global', 'boot_count']:
|
||||
text = '12'
|
||||
else:
|
||||
text = ''
|
||||
return subprocess.CompletedProcess(argv, 0, text, 'PRIVATE_STDERR')
|
||||
with patch.object(module.subprocess, 'run', side_effect=run):
|
||||
result = module.collect('adb', 'test-device', True)
|
||||
self.assertEqual(result['kernel']['status'], 'requires_user_unlock')
|
||||
self.assertFalse(any('--print' in command for command in commands))
|
||||
self.assertNotIn('PRIVATE_STDERR', json.dumps(result))
|
||||
|
||||
def test_command_failure_does_not_export_stderr(self):
|
||||
with patch.object(module.subprocess, 'run', return_value=subprocess.CompletedProcess([], 1, 'UNRELATED_DATA', 'PRIVATE_STDERR')):
|
||||
result = module.collect('adb', 'test-device', True)
|
||||
self.assertIsNone(result['boot_count'])
|
||||
self.assertNotIn('UNRELATED_DATA', json.dumps(result))
|
||||
self.assertNotIn('PRIVATE_STDERR', json.dumps(result))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user