mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
fix: kill Fusion outline on QScintilla, type inference hints, workspace styling
- Suppress PE_Frame on QsciScintilla in MenuBarStyle to eliminate the 1px dark (#171717) Fusion outline around the editor area - Add --screenshot flag for automated pixel regression testing - Add type inference engine (typeinfer.h) with hex pattern analysis - Show inferred type hints on hex nodes in compose output - Style workspace tree corner/header widgets to match theme - Fix integer overflow in compose.cpp array element addressing - Fix integer overflow in core.h structSpan calculation - Add bounds check on activePaneIdx in controller - Use QPointer for deferred dock lambda safety - Workspace delegate uses icon Normal/Disabled for viewed state
This commit is contained in:
92
tests/test_pixels.py
Normal file
92
tests/test_pixels.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Pixel boundary test: validates no Fusion outline leak at the workspace→editor seam.
|
||||
|
||||
Usage:
|
||||
python tests/test_pixels.py [screenshot.png]
|
||||
|
||||
If no screenshot given, launches Reclass.exe --screenshot to grab one.
|
||||
Scans for the specific Fusion outline artifact: color (23,23,23) which is
|
||||
window.darker(140) for the VS2022 Dark theme background #1e1e1e.
|
||||
"""
|
||||
import sys, os, subprocess
|
||||
from PIL import Image
|
||||
from collections import defaultdict
|
||||
|
||||
GRAB_PATH = os.path.join("build", "test_grab.png")
|
||||
|
||||
def get_screenshot(path):
|
||||
if not os.path.exists(path):
|
||||
print(f"Launching Reclass.exe --screenshot {path}")
|
||||
subprocess.run(["./build/Reclass.exe", "--screenshot", path],
|
||||
timeout=15, check=True)
|
||||
return Image.open(path)
|
||||
|
||||
def scan_for_artifact(img):
|
||||
"""Scan entire image for the Fusion outline color (23,23,23).
|
||||
Also find all near-black pixels (< 28,28,28) that aren't the
|
||||
theme background (30,30,30)."""
|
||||
w, h = img.size
|
||||
px = img.load()
|
||||
|
||||
target = (23, 23, 23)
|
||||
bg = (30, 30, 30)
|
||||
|
||||
target_hits = []
|
||||
dark_hits = defaultdict(list) # color → [(x,y), ...]
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b = px[x, y][:3]
|
||||
if r == target[0] and g == target[1] and b == target[2]:
|
||||
target_hits.append((x, y))
|
||||
elif r < 28 and g < 28 and b < 28 and (r, g, b) != (0, 0, 0):
|
||||
# Near-black but not pure black (text anti-aliasing) and not bg
|
||||
dark_hits[(r, g, b)].append((x, y))
|
||||
|
||||
return target_hits, dark_hits
|
||||
|
||||
def summarize_region(hits):
|
||||
"""Summarize a list of (x,y) hits."""
|
||||
if not hits:
|
||||
return "none"
|
||||
xs = [p[0] for p in hits]
|
||||
ys = [p[1] for p in hits]
|
||||
return (f"{len(hits)}px x=[{min(xs)}..{max(xs)}] y=[{min(ys)}..{max(ys)}] "
|
||||
f"size={max(xs)-min(xs)+1}x{max(ys)-min(ys)+1}")
|
||||
|
||||
def main():
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else GRAB_PATH
|
||||
img = get_screenshot(path)
|
||||
w, h = img.size
|
||||
print(f"Image: {w}x{h}")
|
||||
|
||||
target_hits, dark_hits = scan_for_artifact(img)
|
||||
|
||||
print(f"\n(23,23,23) Fusion outline pixels: {summarize_region(target_hits)}")
|
||||
|
||||
if dark_hits:
|
||||
print(f"\nOther near-black pixels (< 28,28,28):")
|
||||
for c, positions in sorted(dark_hits.items(), key=lambda t: -len(t[1])):
|
||||
print(f" ({c[0]:3},{c[1]:3},{c[2]:3}): {summarize_region(positions)}")
|
||||
|
||||
if target_hits:
|
||||
# Show row distribution (condensed)
|
||||
rows = defaultdict(list)
|
||||
for x, y in target_hits:
|
||||
rows[y].append(x)
|
||||
print(f"\n(23,23,23) row detail:")
|
||||
for y in sorted(rows.keys()):
|
||||
xs = sorted(rows[y])
|
||||
if len(xs) > 5:
|
||||
print(f" y={y}: {len(xs)}px x=[{xs[0]}..{xs[-1]}]")
|
||||
else:
|
||||
print(f" y={y}: {len(xs)}px x={xs}")
|
||||
|
||||
print(f"\nFAIL: Found {len(target_hits)} Fusion outline pixels (23,23,23)")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("\nPASS: No Fusion outline artifact found")
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
189
tests/test_typeinfer.cpp
Normal file
189
tests/test_typeinfer.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <cstring>
|
||||
#include "typeinfer.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestTypeInfer : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
|
||||
// ── NULL / zero → empty ──
|
||||
|
||||
void nullPtr() {
|
||||
QVERIFY(inferTypes(nullptr, 8).isEmpty());
|
||||
}
|
||||
void zeroLen() {
|
||||
uint8_t d[4] = {};
|
||||
QVERIFY(inferTypes(d, 0).isEmpty());
|
||||
}
|
||||
void allZeros8() {
|
||||
uint8_t d[8] = {};
|
||||
QVERIFY(inferTypes(d, 8).isEmpty());
|
||||
}
|
||||
void allZeros4() {
|
||||
uint8_t d[4] = {};
|
||||
QVERIFY(inferTypes(d, 4).isEmpty());
|
||||
}
|
||||
void allZeros2() {
|
||||
uint8_t d[2] = {};
|
||||
QVERIFY(inferTypes(d, 2).isEmpty());
|
||||
}
|
||||
|
||||
// ── Hex64: float pair ──
|
||||
// {21.0488f, 547.3f} — two clear floats with fractional parts;
|
||||
// whole-width Double/Ptr64 score poorly → Float×2 dominates
|
||||
void hex64_floatPair() {
|
||||
float a = 21.0488f, b = 547.3f;
|
||||
uint8_t d[8];
|
||||
std::memcpy(d, &a, 4);
|
||||
std::memcpy(d + 4, &b, 4);
|
||||
auto r = inferTypes(d, 8);
|
||||
QVERIFY(!r.isEmpty());
|
||||
auto& top = r[0];
|
||||
QCOMPARE(top.kinds.size(), 2);
|
||||
QCOMPARE(top.kinds[0], NodeKind::Float);
|
||||
QVERIFY(top.strength >= 3); // strong
|
||||
}
|
||||
|
||||
// ── Hex64: int32 pair ──
|
||||
// {42, 99} — two small integers
|
||||
void hex64_intPair() {
|
||||
int32_t a = 42, b = 99;
|
||||
uint8_t d[8];
|
||||
std::memcpy(d, &a, 4);
|
||||
std::memcpy(d + 4, &b, 4);
|
||||
auto r = inferTypes(d, 8);
|
||||
QVERIFY(!r.isEmpty());
|
||||
auto& top = r[0];
|
||||
QVERIFY(top.kinds.size() == 2);
|
||||
QVERIFY(top.kinds[0] == NodeKind::Int32 || top.kinds[0] == NodeKind::UInt32);
|
||||
}
|
||||
|
||||
// ── Hex64: UTF-8 string ──
|
||||
void hex64_utf8() {
|
||||
uint8_t d[8] = {'I', 'C', 'h', 'o', 'o', 's', 'e', 'Y'};
|
||||
auto r = inferTypes(d, 8);
|
||||
QVERIFY(!r.isEmpty());
|
||||
// Top should be UTF8 (strong)
|
||||
bool foundUtf8 = false;
|
||||
for (const auto& s : r) {
|
||||
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::UTF8) {
|
||||
foundUtf8 = true;
|
||||
QVERIFY(s.strength >= 3); // strong
|
||||
}
|
||||
}
|
||||
QVERIFY(foundUtf8);
|
||||
}
|
||||
|
||||
// ── Hex64: pointer-like value ──
|
||||
void hex64_pointer() {
|
||||
// 0x00007FF6A0B01000 — typical Windows user-mode address
|
||||
uint8_t d[8] = {0x00, 0x10, 0xB0, 0xA0, 0xF6, 0x7F, 0x00, 0x00};
|
||||
auto r = inferTypes(d, 8);
|
||||
QVERIFY(!r.isEmpty());
|
||||
bool foundPtr = false;
|
||||
for (const auto& s : r)
|
||||
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::Pointer64)
|
||||
foundPtr = true;
|
||||
QVERIFY(foundPtr);
|
||||
}
|
||||
|
||||
// ── Hex32: clear float ──
|
||||
void hex32_float() {
|
||||
// 21.0488f = 0x41A86600
|
||||
uint8_t d[4] = {0x00, 0x66, 0xA8, 0x41};
|
||||
auto r = inferTypes(d, 4);
|
||||
QVERIFY(!r.isEmpty());
|
||||
QCOMPARE(r[0].kinds.size(), 1);
|
||||
QCOMPARE(r[0].kinds[0], NodeKind::Float);
|
||||
QVERIFY(r[0].strength >= 2);
|
||||
}
|
||||
|
||||
// ── Hex32: small integer with monotonic history ──
|
||||
void hex32_int_monotonic() {
|
||||
// Value: 0x0000BFFC = 49148 (signed: 49148)
|
||||
uint8_t d[4] = {0xFC, 0xBF, 0x00, 0x00};
|
||||
InferHints h;
|
||||
h.monotonic = true;
|
||||
h.sampleCount = 10;
|
||||
uint8_t minB[4] = {0x10, 0x00, 0x00, 0x00}; // 16
|
||||
uint8_t maxB[4] = {0xFC, 0xBF, 0x00, 0x00}; // 49148
|
||||
h.minObserved = minB;
|
||||
h.maxObserved = maxB;
|
||||
auto r = inferTypes(d, 4, h);
|
||||
QVERIFY(!r.isEmpty());
|
||||
QVERIFY(r[0].kinds[0] == NodeKind::Int32 || r[0].kinds[0] == NodeKind::UInt32);
|
||||
QVERIFY(r[0].strength >= 2);
|
||||
}
|
||||
|
||||
// ── Hex16: small unsigned ──
|
||||
void hex16_uint() {
|
||||
uint8_t d[2] = {0x5F, 0x00}; // 95
|
||||
auto r = inferTypes(d, 2);
|
||||
QVERIFY(!r.isEmpty());
|
||||
QVERIFY(r[0].kinds[0] == NodeKind::Int16 || r[0].kinds[0] == NodeKind::UInt16);
|
||||
}
|
||||
|
||||
// ── Hex8: bool-like ──
|
||||
void hex8_bool() {
|
||||
uint8_t d[1] = {1};
|
||||
auto r = inferTypes(d, 1);
|
||||
QVERIFY(!r.isEmpty());
|
||||
bool foundBool = false;
|
||||
for (const auto& s : r)
|
||||
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::Bool)
|
||||
foundBool = true;
|
||||
QVERIFY(foundBool);
|
||||
}
|
||||
|
||||
// ── formatHint ──
|
||||
void formatHint_single() {
|
||||
TypeSuggestion s;
|
||||
s.kinds = {NodeKind::Float};
|
||||
QCOMPARE(formatHint(s), QStringLiteral("float"));
|
||||
}
|
||||
void formatHint_split() {
|
||||
TypeSuggestion s;
|
||||
s.kinds = {NodeKind::Float, NodeKind::Float};
|
||||
QString h = formatHint(s);
|
||||
QVERIFY(h.contains("float"));
|
||||
QVERIFY(h.contains("2"));
|
||||
}
|
||||
|
||||
// ── Denormal rejection ──
|
||||
void denormalRejected() {
|
||||
// Denormal float: exp=0, mantissa non-zero → 0x00000001
|
||||
uint8_t d[4] = {0x01, 0x00, 0x00, 0x00};
|
||||
auto r = inferTypes(d, 4);
|
||||
// Should NOT suggest Float as top pick
|
||||
if (!r.isEmpty() && r[0].kinds.size() == 1)
|
||||
QVERIFY(r[0].kinds[0] != NodeKind::Float);
|
||||
}
|
||||
|
||||
// ── Benchmark: single call ──
|
||||
void bench_singleCall() {
|
||||
uint8_t d[8] = {0x00, 0x00, 0x80, 0x3F, 0xCD, 0xCC, 0x4C, 0x3E};
|
||||
QBENCHMARK {
|
||||
inferTypes(d, 8);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Benchmark: 200-node batch (simulates one refresh) ──
|
||||
void bench_batchRefresh() {
|
||||
// Prepare 200 varied byte patterns
|
||||
QVector<std::array<uint8_t, 8>> data(200);
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
uint32_t seed = (uint32_t)(i * 7919 + 1);
|
||||
for (int j = 0; j < 8; ++j)
|
||||
data[i][j] = (uint8_t)((seed >> (j * 3)) ^ (i + j));
|
||||
}
|
||||
QBENCHMARK {
|
||||
for (int i = 0; i < 200; ++i)
|
||||
inferTypes(data[i].data(), 8);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestTypeInfer)
|
||||
#include "test_typeinfer.moc"
|
||||
Reference in New Issue
Block a user