XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
"""Native website-style Markdown/code proof using actual QMP input and pixels.

The guest exports fixed read-only DOM diagnostics. This harness never injects
source, changes guest DOM through a debugger, or substitutes a host screenshot.
"""
import hashlib
import json
from pathlib import Path
import re
import time
from urllib.parse import urlsplit

from PIL import Image, ImageChops
from NativeCSharpEvidence import normalized_color


ROOT = Path(__file__).resolve().parents[1]
DOCUMENT = re.compile(r'XFE_WEBSITE_DOCUMENT_PROOF_BEGIN\r?\n(.*?)\r?\nXFE_WEBSITE_DOCUMENT_PROOF_END', re.S)
WINDOW = re.compile(r'^XFE_WINDOW_STATE\|(\d+)\|Browser\|(-?\d+)\|(-?\d+)\|(\d+)\|(\d+)\|([01])\|([01])\|([01])$', re.M)
RENDER_FRAMES = re.compile(r'XFE_RENDER_STATS\|\d+\|Frames\|(\d+)\|')


def wait_document(text, start, predicate=lambda proof: True, timeout=90):
    deadline = time.monotonic() + timeout
    failure = 'No complete native website-document readback'
    while time.monotonic() < deadline:
        for match in reversed(list(DOCUMENT.finditer(text()[start:]))):
            try:
                proof = json.loads(match.group(1))
                if predicate(proof):
                    verify_base(proof)
                    return proof
            except (AssertionError, ValueError, KeyError, TypeError) as error:
                failure = str(error)
        assert all(marker not in text() for marker in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION',
                                                       'XFE_RUNTIME_OOM|', 'XFE_RUNTIME_STACK_OOM|')), text()[-4000:]
        time.sleep(.1)
    raise AssertionError('Actual native website document did not stabilize: ' + failure)


def verify_base(proof):
    assert proof['Kind'] == 'ActualWebsiteDocumentDomReadback' and proof['ReadOnly'] is True
    assert proof['NoHostScreenshotSubstitution'] is True
    assert urlsplit(proof['Address']).netloc == 'xfesystem.local'
    assert proof['Article'] and proof['Article']['Bounds']['Width'] > 0
    styles = {urlsplit(value).path for value in proof['Styles']}
    scripts = {urlsplit(value).path for value in proof['Scripts']}
    assert {'/css/docs/code-style.css', '/css/docs/markdown-style.css', '/css/docs/docs-layout.css', '/css/theme-variables.css'}.issubset(styles)
    assert {'/js/docs/highlight.min.js', '/js/docs/csharp.js', '/js/docs/doc-utils.js', '/js/docs/code-block-tools.js'}.issubset(scripts)
    assert proof['WebsiteHighlighterAvailable'] is True and proof['WebsiteCodeToolsAvailable'] is True


def verify_code(proof, language, source):
    verify_base(proof)
    code = next(block for block in proof['CodeBlocks'] if 'language-' + language in block['CodeClasses'].split())
    assert code['CodeText'] == source, ('Actual native source preview differs from the editor model', source, code['CodeText'])
    assert code['Tokens'] and any('hljs-keyword' in span['Classes'].split() for span in code['Tokens'])
    assert len(code['Tokens']) >= 3
    assert {button['Action'] for button in code['Buttons']} == {'lines', 'download', 'expand', 'copy'}
    assert code['LineNumberCount'] == max(1, len(source.rstrip('\n').split('\n')))
    assert code['CodeBounds']['Width'] > 0 and code['CodeBounds']['Height'] > 0
    return code


def browser_geometry(text):
    matches = list(WINDOW.finditer(text()))
    assert matches, 'No native browser window geometry'
    parts = matches[-1].groups()
    window = {'Id': int(parts[0]), 'X': int(parts[1]), 'Y': int(parts[2]), 'Width': int(parts[3]), 'Height': int(parts[4])}
    viewport = [window['X'] + 2, window['Y'] + 67, window['Width'] - 4, window['Height'] - 96]
    return window, viewport


def paint_evidence(frame, proof, viewport, scale, require_heading=False):
    vx, vy, width, height = viewport
    visible = [0, 0, width, height]
    counts = []
    candidates = []
    if proof.get('Heading'):
        candidates.append(('Heading', proof['Heading']['Text'], proof['Heading']['Bounds'], proof['Heading']['Style']['Color']))
    for block in proof.get('CodeBlocks', []):
        candidates.extend(('CodeToken', span['Text'], span['Bounds'], span['Color']) for span in block['Tokens'] if span['Text'].strip())
    for kind, source, bounds, css_color in candidates:
        left = max(visible[0], bounds['X']); top = max(visible[1], bounds['Y'])
        right = min(width, bounds['X'] + bounds['Width']); bottom = min(height, bounds['Y'] + bounds['Height'])
        if right - left <= 1 or bottom - top <= 1:
            continue
        rgb = tuple(int(normalized_color(css_color)[index:index + 2], 16) for index in (0, 2, 4))
        physical = [round((vx + left) * scale), round((vy + top) * scale), round((vx + right) * scale), round((vy + bottom) * scale)]
        crop = frame.crop(physical).convert('RGB')
        pixels = sum(all(abs(pixel[channel] - rgb[channel]) <= 12 for channel in range(3)) for pixel in crop.getdata())
        counts.append({'Kind': kind, 'Text': source, 'ExpectedCssColor': normalized_color(css_color), 'PhysicalBounds': physical, 'MatchingPixels': pixels})
    heading = any(item['Kind'] == 'Heading' and item['MatchingPixels'] >= 4 * scale * scale for item in counts)
    token_count = sum(item['Kind'] == 'CodeToken' and item['MatchingPixels'] >= 2 * scale * scale for item in counts)
    return {'Passed': (heading if require_heading else token_count >= 2), 'HeadingPainted': heading,
            'VisibleTokenSpansWithActualInk': token_count, 'Samples': counts,
            'Meaning': 'Observed framebuffer glyph ink in actual laid-out DOM rectangles; offscreen tokens do not count.'}


def required_token_evidence(measured, texts, scale):
    """Short non-overlapping anchors cannot pass from a stale shifted code row."""
    threshold = 2 * scale * scale
    samples = [next((item for item in measured['Samples'] if item['Kind'] == 'CodeToken' and item['Text'] == token),
                    {'Kind': 'CodeToken', 'Text': token, 'MatchingPixels': 0}) for token in texts]
    return {'Passed': all(item['MatchingPixels'] >= threshold for item in samples),
            'MinimumMatchingPixelsPerToken': threshold, 'Samples': samples}


def wait_document_presented_frame(text, start, proof, before_frames, evidence, timeout=60):
    # The guest emits this readback before Render(), so observing DOM state is
    # not presentation evidence. Bind the ack to the complete matching END.
    document_end = None
    for match in reversed(list(DOCUMENT.finditer(text()[start:]))):
        try:
            if json.loads(match.group(1)) == proof:
                document_end = start + match.end()
                break
        except (ValueError, TypeError):
            continue
    assert document_end is not None, 'The changed document has no complete matching serial END'
    evidence.update({'DocumentEndOffset': document_end, 'BeforeFrames': before_frames,
                     'DeadlineSeconds': timeout, 'Passed': False})
    deadline = time.monotonic() + timeout
    while True:
        current = text()
        reports = list(RENDER_FRAMES.finditer(current[document_end:]))
        evidence['ObservedFramesAfterDocumentEnd'] = [int(report.group(1)) for report in reports]
        if reports and int(reports[-1].group(1)) > before_frames:
            evidence.update({'AfterFrames': int(reports[-1].group(1)), 'Passed': True})
            return deadline
        assert all(marker not in current for marker in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION',
                                                       'XFE_RUNTIME_OOM|', 'XFE_RUNTIME_STACK_OOM|')), current[-4000:]
        assert time.monotonic() < deadline, ('No actual completed native frame after changed document END', evidence)
        time.sleep(.1)


def capture_document(screenshot, name, text, proof, scale, require_heading=False, timeout=60,
                     required_tokens=(), different_from=None, attempt_log=None):
    _, viewport = browser_geometry(text)
    deadline = time.monotonic() + timeout
    attempts = 0
    captures = []
    while True:
        attempts += 1
        frames = RENDER_FRAMES.findall(text())
        before_frames = int(frames[-1]) if frames else None
        path = Path(screenshot(name + '-' + str(attempts))).resolve()
        with Image.open(path) as frame:
            measured = paint_evidence(frame, proof, viewport, scale, require_heading)
            anchors = required_token_evidence(measured, required_tokens, scale)
            difference_bounds = None
            if different_from is not None:
                x, y, width, height = [round(value * scale) for value in viewport]
                with Image.open(different_from) as before:
                    difference_bounds = ImageChops.difference(before.crop((x,y,x+width,y+height)).convert('RGB'),
                                                              frame.crop((x,y,x+width,y+height)).convert('RGB')).getbbox()
        frames = RENDER_FRAMES.findall(text())
        capture = {'Path': str(path), 'Sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
                   'FrameCountBeforeCapture': before_frames, 'FrameCountAfterCapture': int(frames[-1]) if frames else None,
                   'PixelEvidencePassed': measured['Passed'], 'RequiredTokenEvidence': anchors,
                   'ViewportDifferenceBounds': difference_bounds, 'DifferenceRequired': different_from is not None}
        capture['Passed'] = measured['Passed'] and anchors['Passed'] and (different_from is None or difference_bounds is not None)
        captures.append(capture)
        if attempt_log is not None:
            attempt_log.append(capture)
        if capture['Passed'] or time.monotonic() >= deadline:
            break
        time.sleep(.2)
    assert capture['Passed'], ('Actual website DOM preview has no corresponding completed native framebuffer', capture, measured)
    return {'Path': str(path), 'Sha256': capture['Sha256'], 'BrowserViewport': viewport,
            'PixelEvidence': measured, 'RequiredTokenEvidence': anchors, 'Attempts': attempts, 'CaptureAttempts': captures,
            'ViewportDifferenceBounds': difference_bounds}


def light_article_background(frame, proof, viewport, scale):
    """Sample only real empty article-top pixels, away from text/corners/pointer.

    The threshold permits the website's subtle purple gradient; it does not
    require flat pure white or replace the separate pixel-difference report.
    """
    article=proof['Article']['Bounds'];vx,vy,width,height=viewport
    assert proof['Theme']=='light'
    points=[]
    for fraction in (None,.25,.5,.75,False):
        x=article['X']+(20 if fraction is None else article['Width']-20 if fraction is False else article['Width']*fraction)
        y=article['Y']+(12 if fraction in (None,False) else 15)
        assert 0 <= x < width and 0 <= y < height, ('Light article sample outside real viewport',x,y,viewport)
        px,py=round((vx+x)*scale),round((vy+y)*scale)
        crop=frame.crop((px-1,py-1,px+2,py+2)).convert('RGB')
        minimum=[min(pixel[channel] for pixel in crop.getdata()) for channel in range(3)]
        mean=[sum(pixel[channel] for pixel in crop.getdata())/9 for channel in range(3)]
        points.append({'CssPosition':[x,y],'NativePosition':[px,py],'PatchSizePhysical':[3,3],
                       'MinimumRgb':minimum,'MeanRgb':mean,
                       'Passed':min(minimum)>=220 and minimum[2]>=230})
    return {'Passed':all(point['Passed'] for point in points),'Theme':'light','Samples':points,
            'Threshold':'Each empty-top patch RGB minimum >= 220 and blue >= 230; non-flat light gradient is allowed',
            'OriginalScreenshotOnly':True,'Meaning':'Regression guard against opaque black gradient fallback; not a claim of exact website pixel parity.'}


def run_fixture_checks(qmp, text, screenshot, scale, pointer):
    """Continue from an existing Browser window; navigate the real fixture URL."""
    checks = []
    screenshots = []
    attempt_log = []
    evidence = {'Kind': 'NativeWebsiteMarkdownFixture', 'Passed': False, 'Checks': checks,
                'Screenshots': screenshots, 'HostReferenceCountsAsNativeProof': False,
                'ExactWebsitePixelParityAccepted': False, 'FramebufferCaptureAttempts': attempt_log}
    output = None
    def key(code, down):
        qmp('input-send-event', {'events': [{'type': 'key', 'data': {'down': down, 'key': {'type': 'qcode', 'data': code}}}]});time.sleep(.03)
    def tap(code):
        key(code, True);key(code, False)
    def chord(modifier, code):
        key(modifier, True);tap(code);key(modifier, False)
    def move(x, y):
        target=[round(x*scale), round(y*scale)]
        while pointer != target:
            dx=max(-80,min(80,target[0]-pointer[0]));dy=max(-80,min(80,target[1]-pointer[1]))
            qmp('input-send-event',{'events':[{'type':'rel','data':{'axis':'x','value':dx}},{'type':'rel','data':{'axis':'y','value':dy}}]})
            pointer[0]+=dx;pointer[1]+=dy;time.sleep(.02)
    def click(x, y):
        move(x,y)
        for down in (True,False):
            qmp('input-send-event',{'events':[{'type':'btn','data':{'down':down,'button':'left'}}]});time.sleep(.1)
    def capture(name, proof, require_heading=True, **options):
        nonlocal output
        item=capture_document(screenshot,name,text,proof,scale,require_heading=require_heading,attempt_log=attempt_log,**options)
        screenshots.append(item);output=Path(item['Path']).parent
        return item
    try:
        start=len(text());chord('ctrl','l')
        for character in 'about:Documents':
            if character==':':chord('shift','semicolon')
            elif character.isupper():chord('shift',character.lower())
            else:tap(character)
        tap('ret')
        proof=wait_document(text,start,lambda item:(item.get('Heading') or {}).get('Text')=='XFESystem 文档预览')
        assert proof['Theme']=='light', 'Controlled screenshot fixture must declare its actual light theme'
        fixture_path=ROOT/'Source/XFESystem.Documents/Assets/Website/Fixtures/WebsiteParity.md'
        fixture=fixture_path.read_text('utf-8-sig').replace('\r\n','\n')
        expected_code=re.search(r'```csharp\n(.*?)\n```',fixture,re.S).group(1)+'\n'
        code=verify_code(proof,'csharp',expected_code)
        assert proof['Table'] and 'Language' in proof['Table']['Text']
        assert proof['Blockquote'] and 'Markdown' in proof['Blockquote']['Text']
        assert any(span['Text']=='public' and normalized_color(span['Color'])=='569cd6' for span in code['Tokens'])
        assert any('hljs-type' in span['Classes'].split() and normalized_color(span['Color'])=='4ec9b0' for span in code['Tokens'])
        evidence['FixtureSha256']=hashlib.sha256(fixture_path.read_bytes()).hexdigest()
        evidence['FixturePath']=str(fixture_path)
        evidence['InitialDomReadback']=proof
        checks.append('actual native generated Markdown fixture uses the website component and canonical CSS/JS assets with unchanged C# source and token colors')
        window,viewport=browser_geometry(text)
        frames=RENDER_FRAMES.findall(text());before_frames=int(frames[-1]) if frames else 0
        after_frames=before_frames;settle_steps=[]
        # Rendering is event-driven. Two separate physical pointer movements in
        # the address toolbar each require a subsequent real render-stat ack;
        # idle frames are neither assumed nor fabricated.
        for offset in (20,22):
            serial_start=len(text());prior_frames=after_frames
            point=(window['X']+window['Width']-offset,window['Y']+50)
            if pointer==[round(point[0]*scale),round(point[1]*scale)]:point=(point[0]-1,point[1])
            move(*point)
            settle_deadline=time.monotonic()+12
            while time.monotonic()<settle_deadline:
                reports=list(RENDER_FRAMES.finditer(text()[serial_start:]))
                if reports and int(reports[-1].group(1))>=prior_frames+1:
                    after_frames=int(reports[-1].group(1));break
                time.sleep(.1)
            assert after_frames>=prior_frames+1, ('No actual native frame after a real out-of-article pointer movement',prior_frames,after_frames,point)
            settle_steps.append({'PointerCssPosition':list(point),'BeforeFrames':prior_frames,'AfterFrames':after_frames})
        assert after_frames>=before_frames+2
        evidence['InitialRenderSettled']={'BeforeFrames':before_frames,'AfterFrames':after_frames,'AtLeastTwoFrames':True,
            'AcknowledgedPointerMovements':settle_steps,'PointerCssPosition':list(point),'PointerOutsideArticle':True}
        first=capture('Browser-Website-Markdown-Ready',proof)
        evidence['InitialFramebuffer']=first
        with Image.open(first['Path']) as frame:
            background=light_article_background(frame,proof,viewport,scale)
        evidence['LightArticleBackground']=background
        assert background['Passed'], ('Native light article background is dark despite readable DOM colors',background)
        checks.append('five empty article-top patches preserve the actual light gradient brightness after two native render frames with pointer outside content')
        button=next(button for button in code['Buttons'] if button['Action']=='lines')
        rectangle=button['Bounds']
        # A narrower original browser window can put the code below the fold.
        # Scroll through the real mouse path, then re-read actual viewport rects.
        if not (0 <= rectangle['Y'] < viewport[3]-24) or first['PixelEvidence']['VisibleTokenSpansWithActualInk'] < 2:
            move(viewport[0]+viewport[2]/2,viewport[1]+viewport[3]/2)
            for _ in range(max(1,min(12,int(max(0,rectangle['Y']-80)/60)))):
                qmp('input-send-event',{'events':[{'type':'btn','data':{'down':True,'button':'wheel-down'}}]});time.sleep(.05)
                qmp('input-send-event',{'events':[{'type':'btn','data':{'down':False,'button':'wheel-down'}}]});time.sleep(.1)
            scroll_start=len(text());move(window['X']+window['Width']-20,window['Y']+50)
            proof=wait_document(text,scroll_start,lambda item:any(0 <= next((button['Bounds']['Y'] for button in block['Buttons'] if button['Action']=='lines'),-1) < viewport[3]-24 for block in item.get('CodeBlocks',[])),timeout=20)
            code=verify_code(proof,'csharp',expected_code)
            button=next(button for button in code['Buttons'] if button['Action']=='lines');rectangle=button['Bounds']
        code_frame=capture('Browser-Website-Markdown-Code',proof,require_heading=False,required_tokens=('using','System'))
        evidence['CodeFramebuffer']=code_frame
        checks.append('actual native Markdown heading and visible website code-token rectangles contain matching framebuffer glyph colors')
        assert 0 <= rectangle['X'] < viewport[2] and 0 <= rectangle['Y'] < viewport[3], ('Real line-number button is outside the initial viewport',rectangle,viewport)
        assert 'code-lines-visible' in code['Classes'].split() and button['Pressed']=='true'
        frames=RENDER_FRAMES.findall(text());before_toggle_frames=int(frames[-1]) if frames else 0
        start=len(text());click(viewport[0]+rectangle['X']+rectangle['Width']/2,viewport[1]+rectangle['Y']+rectangle['Height']/2)
        changed=wait_document(text,start,lambda item:any('code-lines-visible' not in block['Classes'].split() for block in item.get('CodeBlocks',[])))
        changed_code=verify_code(changed,'csharp',expected_code)
        assert next(button for button in changed_code['Buttons'] if button['Action']=='lines')['Pressed']=='false'
        assert changed_code['CodeBounds']['X'] < code['CodeBounds']['X'], ('Actual line-number click changed state but not the web layout',code['CodeBounds'],changed_code['CodeBounds'])
        evidence['LinesHiddenDomReadback']=changed
        presented={};evidence['LinesHiddenPresentation']=presented
        deadline=wait_document_presented_frame(text,start,changed,before_toggle_frames,presented)
        move(window['X']+window['Width']-20,window['Y']+50)
        second=capture('Browser-Website-Markdown-LinesHidden',changed,require_heading=False,
                       required_tokens=('using','System'),different_from=code_frame['Path'],
                       timeout=max(.001,deadline-time.monotonic()))
        with Image.open(code_frame['Path']) as before, Image.open(second['Path']) as after:
            bounds=code_frame['BrowserViewport'];x,y,w,h=[round(value*scale) for value in bounds]
            difference=ImageChops.difference(before.crop((x,y,x+w,y+h)).convert('RGB'),after.crop((x,y,x+w,y+h)).convert('RGB'))
            assert difference.getbbox() is not None, 'Real line-number click produced no native framebuffer change'
        checks.append('real mouse hit invokes the original website line-number button event, changes aria/class, relayouts code and repaints native pixels')
        evidence['ObservedFeatureGaps']=[]
        if proof.get('KatexCount',0)==0:evidence['ObservedFeatureGaps'].append('The fixture math did not produce KaTeX DOM')
        if proof.get('MermaidSvgCount',0)==0:evidence['ObservedFeatureGaps'].append('The fixture diagram did not produce a Mermaid SVG')
        evidence['MathDomCount']=proof.get('KatexCount',0);evidence['MermaidSvgCount']=proof.get('MermaidSvgCount',0)
        evidence['Boundary']='Core Markdown/code/toolbar behavior checked. Font/raster differences and optional math/diagram gaps are reported separately; passing is not a claim of complete website pixel parity.'
        evidence['Passed']=True
        return evidence
    except Exception as error:
        evidence['Failure']=str(error)
        raise
    finally:
        if output is not None:
            path=output/'WebsiteDocumentEvidence.json'
            path.write_text(json.dumps(evidence,ensure_ascii=False,indent=2)+'\n','utf-8')
            evidence['EvidencePath']=str(path);evidence['EvidenceSha256']=hashlib.sha256(path.read_bytes()).hexdigest()