폼 검증
이 계약은 중첩 폼의 HTTP, 브라우저, 영속 저장 검증을 정의합니다. 구현과 검증기는 이 저장소에서 관리합니다. 검증은 커밋 여부와 관계없이 현재 저장소 트리를 대상으로 실행하며 그 트리의 소스 식별자를 기록합니다. 다른 체크아웃의 소스 파일은 읽지 않습니다.
정본 예제와 벤치마크 경계
공개 루트는 CRUDUI 전체 기능 정본 예제입니다. 아래의 레코드 리소스 하나로 하나의 레코드
흐름 List → Detail → Form → Save → List refresh를 보여 줍니다. 그 레코드의 목록·상세·링크·
폼·검증·제출은 CRUDUI가 생성합니다. 페이지는 다섯 서버 js(공개 Node 서버 자신), php,
php-ext(CRUDUI·OrderedJSON 확장을 로드한 PHP), go, rust 가운데 하나, 네 클라이언트
html, react, vue, svelte 가운데 하나, 초기화 방식 csr 또는 ssr, 폼 모드
bindForm 또는 createForm을 선택합니다. 외부 단계 버튼, 부모 쪽 가짜 단계 라우팅, 폼
프레임은 페이지에 포함하지 않습니다.
/benchmark-console/과 /benchmark/는 별도의 검증 화면입니다. 중첩 회사 시나리오
(src/scenario.mjs), 렌더·검증 매트릭스, 초기화 방식 나란히 비교, 벤치마크 조작은 이 화면들이
소유하며 회사 시나리오는 다른 곳에서 쓰지 않습니다. 정본 페이지는 이 가운데 어느 것도 포함하지
않습니다. /displays/는 경로로 제공하지 않습니다.
레코드 리소스
명세. examples/form-comparison/fixtures/customer-specs.json은 고객 레코드의 세 명세
list, detail, form을 담습니다. 레코드 항목은 id, name, status, joined, score,
relation.name, avatar, markup, companies입니다. 목록은 companies를 제외한 모든 항목을
보여 주고 각 레코드를 상세로 연결합니다. 상세는 avatar와 companies를 제외한 모든 항목을 보여
주고 레코드를 폼으로 연결합니다. 폼은 name, status, joined, score, relation.name,
markup, companies를 편집하고 id는 읽기 전용(dummy-input)으로 보여 주며 avatar 필드는
없습니다. 목록은 정렬을 선언하지 않고 저장 순서로 레코드를 보여 줍니다.
companies는 폼의 반복 부분입니다. 회사(최대 4개, 복사·정렬, 고정 머리, 필수 name)마다
스토어(같은 행 작업, 필수 name, 기본값 1인 체크박스 enabled, .enabled가 켜진 동안만 보이는
메모 detail, ko·en 제목 title)가 있고, 스토어마다 부서(name)가 있습니다. 값은 키가 있는
행입니다. { key: { name, stores: { key: { name, enabled, detail, title: { ko, en }, departments: { key: { name } } } } } } 형태로 항목은 이 순서이며, 모든 말단은 문자열이고 모든 키는 행 키(__ +
숫자나 소문자 16진 문자 13개 + __)입니다. src/scenario.mjs의 벤치마크 폼은 이 그룹을 같은
파일에서 가져오므로 둘은 이를 한 번만 선언합니다.
고정 데이터. examples/form-comparison/fixtures/customer-records.json은 초기 레코드 45개를
id 순서 "1"~"45"로 담으며 score는 JSON 숫자입니다. 레코드마다 회사·스토어·부서 수가 다르고, 일부
스토어는 enabled가 꺼진 채 detail 문구를 유지합니다. 빌드는 두 파일을 그대로 공개
디렉터리에 customer-records.json과 customer-specs.json으로 게시하고, 모든 서버는 그곳에서
읽습니다. 레코드 사본을 따로 가진 프로그램은 없습니다.
저장소. 서버마다 데이터 디렉터리에 영속 저장소
records-{server}.json 하나를 가집니다. 파일은 고정 데이터의 항목 순서를 유지한 레코드 배열을
id 순서로 담습니다. 파일이 없으면 첫 읽기에서 고정 데이터로 채우고, 읽을 수 없거나 형식이 잘못된 파일은 요청을 500으로
실패시키며 교체하지 않습니다. 고정 데이터의 항목을 순서대로 정확히 가진 레코드의 JSON 배열이고,
score는 숫자, 나머지 스칼라는 문자열, companies는 폼의 형태로 빠짐없이 항목 순서대로 있어야 올바른 파일입니다. 저장 파일은 제출처럼 빈 값으로
채우지 않습니다. 초기화는 형식과
관계없이 저장 파일을 고정 데이터로 교체하며, 저장소를 복구하는 방법입니다. 쓰기는 직렬화합니다. JavaScript 서버는
자기 프로세스 하나 안에서 쓰기를 차례로 처리하고, PHP·Go·Rust 서버는 읽기와 쓰기 동안
records-{server}.json.lock에 배타 잠금을 겁니다. 쓰기는 같은 디렉터리의 임시 파일을 거쳐
파일을 원자적으로 교체합니다. 저장한 값이 다음에 목록·상세·폼에 보이는 값이며 재시작 후에도
같습니다.
HTTP 계약. 아래 경로는 각 서버 자신의 경로입니다. 공개 서버는 php, php-ext, go,
rust의 /api/{server}/records…를 그 서버의 /api/records…로 전달하고
/api/js/records…에는 직접 응답합니다. 공개 서버의 모든 응답은 Cache-Control: no-store와
X-Content-Type-Options: nosniff를 가진 application/json; charset=utf-8 또는
text/html; charset=utf-8이고, 모든 JSON 응답은 <, >, &를 \u003c, \u003e,
\u0026로 이스케이프해 어떤 해석에서도 실패에 반영된 요청 값이 스크립트나 주석 요소를 닫을 수
없습니다. 모든 실패 응답은 { "error": message, "server": … }입니다.
| 요청 | 성공 | 실패 |
|---|---|---|
GET /api/records?page={n} |
200 { page, perPage: 20, total, records }: id 순서의 n쪽 |
질의가 부호·앞자리 0 없는 1 이상의 십진 정수 page 하나가 아니면 400 Expected one page parameter, 마지막 쪽을 넘으면 404 Page not found |
GET /api/records/{id} |
200 { record } |
저장되지 않은 id(0과 앞자리 0이 있는 id 포함)는 404 Record not found |
POST /api/records/{id} |
200 { record, validation: { valid: true, errors: [], hidden } } |
아래 참조 |
POST /api/records/reset |
200 { total: 45 }, 저장소가 고정 데이터와 같아짐 |
본문이 있는 요청은 400 |
GET /api/records/view/{view}?… |
200 { view, html, data } |
아래 참조 |
이 경로에서 다른 메서드는 405로 응답하며, 대상이 받는 메서드를 적은 Allow 헤더를 가집니다(페이지는 GET, 파일은 GET, HEAD).
저장은 렌더링된 폼이 전송하는 네이티브 폼(정확히 form[…] 필드와 _form_complete=1만 담은
multipart/form-data 또는 application/x-www-form-urlencoded)과 정확히 form 항목만 가진 JSON
{ "form": { … } }을 받으며, 다른 필드나 항목이 있으면 400으로 응답합니다. 제출 객체는 폼 항목 id, name, status, joined, score, relation(name 포함),
markup, companies를 담고 스칼라 값은 문자열, companies는 위 형태이며 다른 항목은 없습니다.
id는 반드시 있어야 합니다. 나머지 항목은 빠질 수 있습니다. 폼 데이터는 값이 없는 필드(새 행에서
건드리지 않은 텍스트 필드)를 빼고, 네이티브 폼은 체크 해제된 enabled와 행이 없는 컬렉션을 보내지
않기 때문입니다. 서버는 두 미디어 형식 모두에서 빠진 텍스트·체크박스 항목을 ""로, 빠진 title을
{ "ko": "", "en": "" }로, 빠진 relation을 { "name": "" }로, 빠진 컬렉션을 행 없음으로 채워 같은
레코드를 저장합니다. 같은 companies 데이터에 대한 벤치마크 저장과 검증도 이
규칙을 그대로 적용하므로 서버마다 회사 형태를 한 곳에서 검사합니다. 서버는 알 수 없는 id에 404,
다른 미디어 형식에 415, 2 MiB 초과에 413, 잘못된 입력·완료 필드 누락·반복된 네이티브 필드·id 누락·항목 추가나
문자열이 아닌 항목·행 키가 아닌 키·""나 "1"이 아닌 enabled·경로와 다른 id에 400으로
응답합니다. 2 MiB를 넘는 본문은 제한을 넘는 즉시 413으로 응답하고 더 읽지 않으며 연결을 닫으므로,
클라이언트가 큰 본문을 선언하고 천천히 보내 서버를 붙잡을 수 없습니다. 그다음 자기 검증기와 form 명세로
제출을 검증하고, 검증기 결과를 member valid, errors, hidden을 바꾸지 않은 채 validation으로
응답합니다(검증 결과). 잘못된 제출에는 422 { validation }으로 응답합니다.
올바른 제출에는 200으로 응답하며, 이전 레코드에 제출한 name, status, joined, relation.name, markup, companies(행은
제출 순서와 제출한 키를 유지하고, 스토어의 detail은 숨겨진 동안에도 유지)와 십진 문자열이
나타내는 JSON 숫자 score를 넣어 저장하며, id와 avatar는 저장된 값을
유지합니다. 어떤 실패도 저장 파일을 바꾸지 않습니다. 레코드 목록으로 읽을 수 없는 저장 파일은
500으로 응답하며 그대로 둡니다.
보기 요청은 SSR 문서의 단계 하나를 렌더링합니다. 질의는 정확히 lang(ko 또는 en),
server(응답하는 서버), framework, initialization=ssr, mode, page를 이 순서로 한 번씩 가지며
detail과 form에서는 그 앞에 id를 둡니다. 다른 질의와, 부호나 앞자리 0이 없는 1 이상의 10진 정수가
아닌 id는 400으로 응답합니다. 알 수 없는 보기, 마지막 쪽을 넘는 쪽, 형식은 맞지만 저장되지 않은 id는
404로 응답합니다. /api/records/{id}의 id는 퍼센트 디코딩하지 않은, 쓴 그대로의 경로 조각이므로 %32%32를 포함해 저장된 id가 아닌 조각은 모두 404로 응답합니다. 응답은 다음을 담습니다.
html:list는 그 쪽의 목록을layout: table, 쪽 번호, 저장된 전체 수로 렌더링한 것,detail은 레코드의 상세,form은<form id="record-form" method="post" action="/api/{server}/records/{id}" enctype="multipart/form-data">, 키 접두사form으로 레코드에서 렌더링한 폼,</form>입니다. 목록 링크는/detail?id={id}&, 상세 링크는/form?id={id}&뒤에 선택 질의lang,server,framework,initialization,mode,page를 이 순서로 붙입니다. HTML은 서버 사이에서 바이트 단위로 같습니다.data:list는{ page, perPage, total, records },detail과form은{ record }입니다.
src/record-contract.mjs는 공용 JavaScript 렌더러와 검증기로 위 모든 응답의 기대값을
계산합니다. 다른 서버는 자기 생성기와 검증기를 사용하며 같은 바이트와 결과를 내야 합니다.
레코드 서버. 모든 레코드 서버는 수신 주소, 데이터 디렉터리, 공개 디렉터리, 소스 식별자
파일을 받습니다. Go·Rust 바이너리와 servers/javascript/main.mjs는 이 순서의 인수로 받습니다.
PHP 서버는 PHP를 운영 환경에서 실행하는 방식대로 nginx 뒤의 PHP-FPM입니다.
servers/php/main.mjs {address} {run directory} -- {php-fpm 인수}가 두 설정을 실행 디렉터리에 쓰고
PATH의 php-fpm과 nginx를 시작하며, 둘 다 연결을 받으면 준비 줄을 출력하고 멈출 때 둘 다
멈춥니다. 이를 시간 한도와 연결 시도 없이 두 프로세스가 standard error에 쓰는 자기 log 줄로 압니다.
PHP-FPM은 socket에서 listen한 뒤 NOTICE: ready to handle connections를 쓰고, nginx는 notice log
level에서 listening socket을 연 뒤 start worker processes를 씁니다. program은 수신 주소에 직접 bind하고 그
socket을 변수 NGINX가 가리키는 file descriptor 3으로 nginx에 넘기므로, nginx는 program이 잡은 port에서
수신합니다. nginx는 /api/ 요청을 FastCGI로 servers/php/api.php에 넘기고 다른 경로에는 JSON 404로 응답하며,
2 MiB를 넘는 요청 본문은 PHP가 읽기 전에 계약의 JSON 413으로 끝냅니다. 데이터·공개 디렉터리는
FORM_DATA_DIRECTORY, FORM_PUBLIC_DIRECTORY로 그 스크립트에 전달됩니다. PHP 내장 서버는 스크립트를
실행하기 전에 본문 전체를 읽으므로 너무 큰 요청을 멈출 수 없었습니다. 공개
서버는 주소, 데이터 디렉터리, 공개 디렉터리, 네이티브 서버 포트의 JSON 맵을 인수로 받고,
servers/javascript/main.mjs가 제공하는 모듈과 같은 모듈로 js 레코드에 응답하며, 빌드
상태를 IPC로 받습니다. 각 서버는 port 0을 포함해 요청받은 주소에서 수신을 시작한 뒤 잡은 주소와 함께 준비 줄
CRUDUI_READY {server} {host}:{port}를 출력합니다. local 검사는 모든 서버를 127.0.0.1:0에서 시작하고 그 줄의 주소로
접근하므로, 수신할 서버보다 먼저 port를 고르지 않습니다.
정본 페이지
페이지 주소는 /(목록), /detail, /form 세 개입니다. 질의는 lang, server,
framework, initialization, mode, page를 가지며 detail과 form은 id를, 저장 후의
목록은 saved를 추가로 받습니다. 빠진 선택 항목은 기본값(ko, js, html, csr,
bindForm, 1)을 쓰고, 알 수 없는 값은 400, 마지막 쪽을 넘는 쪽이나 알 수 없는 id는 404로
응답하며, 두 실패 모두 JSON { "error": message }입니다. 페이지와 CRUDUI가 생성하는 모든 링크는 위 순서의 선택 전체를 가지므로 모든
단계에서 쪽과 선택이 유지됩니다. 선택 컨트롤 #server, #framework, #initialization,
#mode와 페이지 이동은 바뀐 항목으로 이동합니다.
문서에는 단계 <section id="stage" data-view="{view}" …> 하나가 있습니다.
- SSR: 공개 서버가 선택한 서버에 보기를 요청(
/api/records/view/…)해html을 단계에 쓰고,data를<,>,&를 JSON 이스케이프로 적은<script type="application/json" id="crudui-stage-data">로 씁니다. 선택한 클라이언트는 아래 어댑터별 하이드레이션 규칙에 따라 같은 데이터로 그 마크업을 이어받습니다. 단계는 view-source에 보이는 초기 HTML에 들어 있습니다. - CSR: 단계는 비어 있고 문서에 단계 데이터가 없습니다. 선택한 클라이언트가 선택한 서버의
JSON(
/api/{server}/records?page=…또는/api/{server}/records/{id})을 요청해 브라우저에서 보기를 렌더링합니다.html은 generator-html의 목록·상세·폼 렌더러를,react,vue,svelte는 각자의List,Detail,Form컴포넌트를 사용합니다. 서버 HTML을 넣지 않습니다.
폼은 선택한 모드로 단계 안에 렌더링합니다. bindForm은 페이지 컨트롤러로,
createForm은 폼 세션으로 동작합니다. #record-form의 submit 이벤트(저장 버튼 또는 필드에서
Enter)는 폼의 네이티브 필드를 multipart/form-data로 /api/{server}/records/{id}에
전송합니다. 200이면 같은 쪽의 /?{selection}&saved={id}로 이동하고, 422면 폼에 검증 결과를
보여 주고 머뭅니다. 요청 실패 또는 200·422 외의 응답은 폼 앞에 페이지 언어의 저장 실패 문구와 실패
내용을 담은 <div id="save-errors" role="alert">를 보여 주고, 입력 값을 그대로 두며, 폼을 다시
제출할 수 있게 둡니다. saved가 있는 목록은 페이지 언어의 저장 알림을 담은
<p id="saved-notice" role="status" data-record-id="{id}">를 보여 주며, SSR에서는 알림이
초기 HTML에 들어 있습니다.
보기가 상호작용 가능해지면 페이지는 자기 창에
{ type: 'crudui:pipeline-ready', view, server, framework, initialization, mode, page, id }를
게시합니다. page는 숫자이고 목록에서 id는 null입니다. 초기화할 수 없는 보기는 스크립트
오류로 실패합니다. 검사는 이 이벤트를 기다리며 DOM으로 준비 상태를 추정하지 않습니다.
문서는 <html data-pipeline-selection="…">에 자기 전체 선택을 담고, 페이지 스크립트는 어떤
멤버가 URL 선택과 다르면 렌더링 전에 실패합니다. 다른 선택의 오래된 문서는 외부 단계 마크업을
넘겨받는 대신 실패합니다.
정본 페이지는 모든 SSR 단계를 선택한 서버에서 받고 모든 레코드를 그 서버의 저장소에서 읽습니다.
공개 서버는 js를 선택했을 때만 단계를 렌더링합니다.
정본 흐름 검사
src/pipeline-flow.mjs는 다섯 서버, 네 클라이언트, 두 초기화 방식의 40개 조합에서 이
페이지를 검사합니다. 모든 서버와 모든 클라이언트가 두 모드를 모두 실행하도록 폼 모드를
번갈아 쓰고, 제출도 저장 버튼과 Enter를 번갈아 씁니다. 각 조합은 자기 서버 저장소 2쪽의
자기 레코드(id 21~28)를 편집하며 다음을 수행합니다.
- 목록·상세·폼 문서를 요청해 SSR 문서에는 렌더링된 단계와 단계 데이터가, CSR 문서에는 단계 데이터 없는 빈 단계가 있는지 확인합니다.
- 2쪽 목록을 열고 레코드의 상세 링크, 이어서 폼 링크를 따라가며 각 주소와 준비 이벤트를 확인합니다.
- 읽기 전용 id를 확인하고 점수를 바꾸고, 첫 회사의 첫 스토어에서
enabled를 해제해detail이 숨겨지는지 확인하고, 그 스토어에 부서 행을 추가해(마지막 부서 행의 추가 작업, 부서가 없으면 컬렉션 바닥의 추가 작업) 이름을 넣은 뒤 제출합니다. 그다음 같은 쪽과saved를 가진 목록 주소, 보이는 레코드 저장 알림, 공용 목록 모델 형식으로 레코드 행에 표시된 새 점수를 확인합니다. - 선택한 서버가 점수,
enabled가""이고detail이 그대로인 그 스토어, 부서의 마지막 행인 새 부서를 저장했는지 확인하고, SSR에서는 목록 초기 HTML에 알림과 점수가 있는지 확인합니다.
각 조합은 자기 제한 시간을 가진 단위 하나이며 자기 브라우저 컨텍스트에서 실행하고, 동시에 넷까지 실행합니다. 단위는 제한 시간을 포함한 시작, 15초마다 경과 시간, 소요 시간을 포함한 결과를 출력합니다. 제한 시간에 도달한 단위는 자기 id와 함께 실패하고 자기 브라우저 컨텍스트를 닫습니다. 조합 하나의 제한 시간은 구현된 흐름의 첫 실행에서 측정한 가장 느린 조합의 열 배이며, 측정 전에는 60초입니다. 조합을 실행하기 전에 서버마다 단위 하나로 공개 API를 통해 저장소를 초기화합니다.
조합은 체크아웃에서 빌드한 로컬 구성(pipeline.browser.mjs, 조합마다 테스트 하나)에 실행하고, 아래
로컬 검증에서는 check-pipeline.mjs로 실행합니다.
record-stores.test.mjs는
체크아웃에서 시작한 다섯 서버 모두에 src/record-contract.mjs의 HTTP 계약 사례를 사례마다 자기
제한 시간으로 실행합니다. 두 로컬 검사는 먼저 서버 프로그램을 빌드하며 각각 시간 한도 없이
진행을 출력하는 단계입니다. 두 PHP 확장, Go 바이너리, Rust 디버그 바이너리를 빌드합니다.
LOCAL=1은 kit의 link providers --ecosystem composer가 출력하는, .links의 checkout이 제공하는
polyspec/ordered-json-php-ext folder를 선택합니다. Extension은 release와 같이 그 package folder에서 빌드하고 Rust 명령은
.links의 checkout을 patch하는 kit의 link cargo로 실행합니다.
LOCAL=1이 없으면 make ordered-json-ext가 PIE로 packages.json 저장소에서 빌드하고 var/ordered-json-package에 기록한 OrderedJSON php-ext 패키지를 명시적 release source로 선택합니다(C17.84).
.links provider나 source가 없으면 실패하며 release archive를 대신 선택하지 않습니다. 그다음 로컬 구성은 다섯 프로세스를 동시에 시작하고 브라우저를 시작합니다.
프로세스 시작과 브라우저 시작은 제한 시간이 없는 장기 작업입니다. 각각 준비 이벤트나 launch에서
끝나고, 기다리는 동안 15초마다 경과 시간 줄을 출력합니다. 이 작업들을 실행하는 테스트 훅은
제한 시간이 없는 setup입니다. 시작한 서버를 멈추지 않고 끝나는 테스트 프로세스는, 예를 들어 실패한 훅과
강제 종료 뒤에도, 종료할 때 그 프로세스 그룹을 멈추므로 어떤 서버도 실행보다 오래 남지 않습니다. npm run test:form-comparison:pipeline이 둘을 실행하고 CI 작업
form-comparison-pipeline이 이 명령을 실행합니다.
진행, 제한 시간, 범위
이 계약의 모든 명령, 곧 로컬 빌드와 로컬 구성과 로컬 검증과 모든 검사에 세 규칙을 적용합니다.
- 실행 중에 무엇을 하는지 보고합니다. 시작과 끝만 남기고 기다리는 검사는 없습니다.
모든 단계는 시작 줄, 실행 중 15초마다 경과 시간 줄, 단계 이름을 붙인 출력, 통과·실패와
소요 시간을 적은 줄을 출력합니다. 빌드
단계, 브라우저 보고서, 검사 구간, 브라우저 시작과 종료(제한 시간이 없는 장기 작업), 정본 흐름 조합, 모든 대기
같은 하위 단위도
[label] unit: started|running|passed|failed|timed out형식의 진행 줄로 같은 방식으로 보고합니다. - 장기 작업에는 시간 한도가 없고, 단위는 자기 제한 시간을 가집니다. 빌드나 검증 검사 같은
단계, 프로세스 시작, 브라우저 시작과 종료, 모든 대기는 장기
작업입니다. 각각 끝까지 실행하며 종료 상태, 준비 이벤트, 결과가 성패를 정하고, 전체 제한 시간도
무활동 제한도 가지지 않습니다. 브라우저 보고서, 검사 구간, 정본 흐름 조합 같은 단위는 짧은
검증이며 가장 느린 측정값의 세 배를 5초 단위로 올린, 최소 10초의 자기 제한 시간을 가집니다
(
measuredLimitMs). 코드는 그 측정 출처를 적습니다. 중지하는 프로세스 트리는 프로세스 그룹과 그룹을 벗어난 자손까지SIGTERM을, 유예 뒤SIGKILL을 받으며, 중단된 검사는SIGKILL을 기다리지 않고SIGTERM에 브라우저를 닫습니다. - 검사는 속한 자리에서 실행합니다. CI는 소스 스위트, 레코드 저장소 계약, Go·Rust 서버 테스트, 정본 흐름 검사, 로컬 검증을 실행합니다. 개발 중에는 변경한 코드의 테스트만 실행합니다.
소스 식별자
트리의 소스 식별자는 체크아웃 커밋과 커밋하지 않은 변경의 SHA-256 digest입니다. digest는
운영자 로컬 편집기 설정을 제외하고 git status가 보고하는 모든
경로를 정렬한 순서로, 경로와 내용 digest
또는 삭제를 포함합니다. 커밋하지 않은 변경이 없는 트리는 changes: null입니다. 식별자
비교는 inode, device, change time을 사용하지 않습니다.
로컬 구성은 식별자를 공개 디렉터리의 source.json에 씁니다. PHP, Go,
Rust 서버는 상태·생성·SSR 응답마다 이 파일을 읽고, 메인 페이지와 프레임은 로드할 때 이 파일을
가져옵니다. 어떤 빌드도 커밋을 포함하지 않습니다.
빌드와 브라우저 준비
make build-php-ext는 polyspec_crudui.so를 phpize, configure, make로 var/php-ext에 빌드합니다. local 실행은
선택한 OrderedJSON extension을 local mode에서는 provider build로 빌드합니다. Release mode는 선언된 build path와
composer.json을 함께 복사한 뒤 phpize, configure, make를 실행합니다. Provider의 composer.json이
php-ext.extension-name, php-ext.build-path, version을 선언하며 이 값이 module 경로와 native PHP process에
전달하는 FORM_JSON_EXTENSION을 결정합니다. 빌드의 PHP는 PATH의 phpize와 php-config가 속한 것입니다. 명령이 실패한 뒤 다른 실행 파일이나 빌드 경로를 선택하지 않습니다.
폼 비교 CI 작업은 소스·생성기 생성 스위트를 실행하기 전에 루트 npm 그래프와 PHP
검증기·생성기의 Composer 그래프를 설치합니다. 깨끗한 체크아웃은 무시된 vendor/
디렉터리를 사용하지 않습니다.
브라우저 검증은 이동 전에 메인 페이지 준비 이벤트를 수신할 호스트 콜백을
등록합니다. 메인 페이지는 초기 비교 프레임 두 개가 준비 이벤트를 게시한 뒤 준비
이벤트를 게시합니다. 검증기는 브라우저 평가 호출을 열린 상태로 유지하지 않고 호스트
콜백을 기다립니다. public/의 모듈은 그대로 제공되며 브라우저가 해석할 수 있는 상대
import만 사용합니다. 페이지가 로드하는 다른 모든 모듈은 build가 bundle합니다. benchmark
console은 그것이 import하는 소스 모듈(browser-job.mjs, frame-readiness.mjs,
frame-pointer.mjs, runtime-paths.mjs, form inspector의 snapshot 모듈)과 함께, 프레임과
페이지 stage도 bundle됩니다. 어떤 소스 모듈도 공개 디렉터리에 복사하지 않습니다. 소스
검증은 public/을 HTTP로 제공하고 페이지 스크립트가 import하는 모듈을 Chromium에서
로드합니다. 초기화할 수 없는 프레임은 준비 이벤트 대신 이유를 게시하고, 페이지는 그 이유로
실패합니다. 메인 페이지
준비 전에 스크립트 오류, 페이지 실패, 페이지 종료가 발생하면 현재 서버 실행을 실패로
처리하고 오류를 기록합니다. 초기화 실패를 확인한 뒤 준비 대기를 계속하지 않습니다.
검증기는 작업을 시작하기 전에 해당 작업과 렌더러 이벤트도 구독합니다. 결과를 읽기 전에
작업 Promise와 렌더러 완료 신호를 기다립니다. 주기적인 DOM 조회나 경과 시간으로 페이지
준비, 프레임 준비, 검증 완료, 렌더링 완료를 추론하지 않습니다. 진행 게시가 중단된
브라우저 작업은 시간 제한으로 실패할 수 있지만 시간 제한 도달을 성공으로 판정하지
않습니다.
구현과 요청
이 절과 이어지는 세 절은 벤치마크 콘솔의 폼 매트릭스를 설명합니다. 정본 페이지는 위의 레코드
리소스를 사용합니다. PHP, PHP 확장, Go, Rust는 동일한 컴파일, 렌더링, 검증, 영속 저장, SSR 요청 계약을
구현합니다. 각 서버는 자체 생성기와 검증기를 사용합니다. PHP 확장 프로세스는
polyspec_crudui.so와 공급자 Composer metadata가 선언한 OrderedJSON 모듈을 로드하고 PHP 프로세스는
두 확장을 로드하지 않습니다. PHP 프로세스는 root Composer 오토로더와 생성기·검증기 source package를
불러옵니다. native 프로세스는 별도 examples/form-comparison/servers/php/native/composer.json profile을
불러옵니다. 준비 단계는 표준 Composer로 이 profile의 잠긴 PHPUnit 의존성과 application PSR-4 class를
실행별 vendor 디렉터리에 설치합니다. 확장이 해당 API를 제공하므로 이 profile에는 PHP 생성기·검증기와
OrderedJSON library가 없습니다. FORM_PHP_AUTOLOAD가 생성된 오토로더를 선택하며 native PHPUnit은
COMPOSER와 COMPOSER_VENDOR_DIR도 받습니다. pure PHPUnit controller는 OrderedJSON 확장을 로드하는
child에 FORM_PHP_NATIVE_AUTOLOAD를 전달합니다. PHP 확장 프로세스에서
Polyspec\Crudui\ 아래에 선언된 모든 클래스는
Polyspec\Crudui\FormComparison\ 아래의 폼 비교 클래스를 빼고 확장의 내부 클래스여야
합니다(ReflectionClass::isInternal()). 프로세스는 생성을 시작할 때와 각 응답 전에 이를 검사하고,
PHP가 소스 파일에서 불러온 클래스가 있으면 그 요청은 HTTP 500과 그 클래스 이름으로 실패합니다.
따라서 PHP 확장은 PHP 라이브러리로 대체되지 않습니다.
PHP 상태·생성·SSR의 모든 출처 응답은 PHP 프로세스에서는 생성기 소스 디렉터리의 Generator와 Form,
검증기 소스 디렉터리의 Validator를, PHP 확장 프로세스에서는 확장을 보고합니다. 결과 검증은 하나의
클래스 위치 계약을 사용하며 이 위치만 허용합니다.
공개 API는 /api/{server}/{action}/{renderingPath}/{framework}만 허용합니다. 한 구성은
PHP 프로세스, PHP 확장 프로세스, Go 프로세스, Rust 프로세스를 각각
하나만 실행합니다. 공개 프로세스는 허용한 요청을 선택한 프로세스의
/api/{action}/{renderingPath}/{framework}로 전달합니다. API에는 구현 리비전이나
데이터 구조 경로가 없습니다. bindForm과 createForm은 렌더링·편집 경로를
선택합니다. 키 객체는 반복 인스턴스 데이터를 정의하며 구현이나 URL을 선택하지
않습니다.
프레임 페이지의 폼에는 novalidate가 있습니다. 프레임은 CRUDUI 검증기로 검증해 그 결과를 보여 주고,
프레임의 검사는 서버에 일부러 잘못된 제출을 보냅니다. 정본 페이지의 폼은 브라우저의 제약 검증을
유지합니다.
ssr 동작은 렌더링 경로와 프레임워크 하나의 프레임 문서를 제공합니다. 쿼리는 정확히
lang(ko 또는 en), server(응답하는 런타임), initialization=ssr이며 각각 한
번씩입니다. 그 밖의 쿼리는 Expected lang, server and initialization for this SSR frame로
거부합니다. 서버는 빌드된 프레임 문서
{public}/frames/{renderingPath}-{framework}/index.html을 읽습니다. 이 문서에는 <html>
시작 태그, 빈 폼 뷰 <div id="form-view"></div>, </body>가 각각 정확히 하나 있어야
하며, 그 밖의 문서는 The frame document must contain one html start tag, one empty form view and one body end tag로 거부합니다. 응답은 그 문서에 세 가지만 삽입하고 나머지는
그대로 둔 것입니다. html 시작 태그의 언어, 폼 뷰 안에 저장 레코드로 렌더링한 폼, body
종료 태그 바로 앞의 <script type="application/json" id="crudui-ssr">에 담은 레코드와
생성기 출처이며, 이 페이로드의 <, >, &는 JSON 이스케이프로 씁니다. 응답은
text/html; charset=utf-8과 Cache-Control: no-store로 보냅니다.
브라우저는 bindForm과 createForm 렌더링 경로를 실행합니다. 두 경로는 동일한
같은 소스 트리의 패키지, 같은 keyed 데이터, 같은 전송 계약을 사용합니다. 선택한 서버가
데이터와 독립된 템플릿 하나를 컴파일하고 브라우저가 이 템플릿을 JSON에서
복원합니다. bindForm 경로는 템플릿과 현재 데이터로 필드 모델을 계산합니다. 해당
응용 프로그램 컨트롤러는 입력과 행 작업 후 keyed 데이터를 갱신하고 필드를 다시
바인딩합니다. createForm 경로는 같은 템플릿으로 편집 폼 세션을 생성하고 데이터
교체와 행 작업에 세션을 사용합니다. 컴파일 실패는 실패로 반환하며 브라우저가 대체
템플릿을 컴파일하지 않습니다.
/benchmark-console/의 비교 화면은 서버, 프레임워크, 렌더링 경로를 선택하고 같은 템플릿, 레코드, 언어로
같은 폼의 서버 렌더링과 클라이언트 렌더링을 좌우에 표시합니다. 왼쪽
프레임(initialization=ssr)은 선택한 서버(PHP, PHP 확장, Go, Rust)의 프레임 문서입니다.
이 문서는 저장 레코드로 렌더링한 폼과 같은 레코드를 담은 페이로드를 함께 전달하고, 선택한
프레임워크가 같은 템플릿과 데이터로 그 폼을 하이드레이션합니다. 하이드레이션 뒤에도 파싱한
폼 DOM, 즉 자식 순서의 모든 요소, 속성 값, 텍스트는 바뀌지 않아야 합니다. 속성 순서는 DOM의 일부가 아니므로
(React는 input의 type, value, name을 다른 속성 뒤에 설정) 비교하지 않으며, 문자열
렌더러의 바이트 동일 HTML은 생성 검사와 네이티브 생성 검사가 확인합니다. style 속성은 CSS 선언 블록이므로 CSS
객체 모델이 직렬화한 선언으로 비교합니다. React는 고정 행의 --crudui-sticky-depth:0을
--crudui-sticky-depth: 0;으로 씁니다. 비교에서 빼는 것은 프레임워크가 렌더링 기준점으로 두며
아무것도 그리지 않는 노드인 주석(Vue)과 빈 텍스트 노드(Svelte)입니다. 비교 대상은 뷰 컨테이너가
담고 있는 것이며 프레임워크가 컨테이너에 쓰는 속성은 아닙니다. 프레임은 각 뷰 컨테이너의
내용만 비교하고 Vue CSR의 data-v-app 같은 프레임워크 표시를 그대로 둡니다. 해당 열이
실제로 하이드레이션했는지는 아래의 프레임 노드 검사가 강제하며 이 비교가 강제하지 않습니다.
각 어댑터는
하이드레이션이 서버 렌더링 노드를 어떻게 다루는지 선언합니다. React, Vue, HTML 렌더러는
그 노드를 이어받으며 프레임은 모든 노드가 그대로 남았는지 확인합니다. Svelte는 자신의 서버
렌더러가 쓰는 <!--[--> 표시가 있는 마크업만 하이드레이션할 수 있고 폼 서버는 그 표시를
쓰지 않으므로, 컨테이너를 비우고 자신의 노드를 마운트합니다. 오른쪽
프레임(initialization=csr)은 빌드된 프레임 문서입니다. 저장 레코드를 요청하기 전에
데이터 없이 폼을 마운트하고, 받은 레코드를 기존 폼에 주입합니다. lang, server,
initialization 중 하나라도 없는 프레임 URL은 실패합니다. 프레임은 페이지 스타일시트보다
먼저 @polyspec/crudui-generator-core/crudui.css를 불러오므로 계산된 CSS를 문법 스타일과 함께
비교합니다. 페이지 스타일시트는 #view 안을 스타일링하지 않습니다. 각 프레임은 비교 대상
요소 안에서 폼을 #form-view에, 브라우저 전용인 구조 맵과 현재 데이터 보기를
#outline-view와 #data-view에 렌더링합니다. 서버는 폼만 렌더링합니다.
모든 렌더 비교에는 React·Vue·Svelte와 함께 프레임워크 독립 HTML 렌더러가 포함됩니다. 따라서 parity 판정 대상은 네 렌더러입니다.
정본 목록의 열은 의미에 따른 정렬을 사용합니다. 식별자·수치·금액은 우측, 상태는 중앙, 날짜는 중앙, 문자·HTML 값은 좌측, 이미지 셀은 가로·세로 모두 중앙에 배치합니다. 표 셀은 세로 중앙 정렬이며, 목록에는 레코드 식별자를 명시적인 순번 열로 포함합니다. 금액 표시는 원본 값에 없는 후행 0을 추가하지 않습니다.
정본 목록은 저장된 레코드 45개를 offset 방식으로 페이지당 20개씩 표시하므로 20개·20개·5개를
담은 세 페이지가 존재합니다. 선택한 페이지는 모든 페이지 주소와 생성 링크에 포함되며, 선택한
서버나 클라이언트는 그 쪽의 목록을 저장된 전체 수로 렌더링합니다. 어떤 렌더러도 total을
현재 페이지의 행 수로 바꾸지 않습니다.
bindForm 컨트롤러는 createForm 인스턴스와 같은 작업을 지원합니다. 행 작업,
toggle-row, select-row, expand-all, collapse-all, undo입니다. 접힌 행과
되돌리기 이력을 keyed 데이터와 분리해 generator-core의 뷰 상태·이력 함수로 유지하고, 폼
런타임의 포커스 규칙에 따라 포커스를 옮기거나 유지합니다.
전체 브라우저 행렬은 시나리오 보고서 64개와 초기화 보고서 32개를 포함합니다.
- 서버 4개: PHP, PHP 확장, Go, Rust
- 프레임워크 4개: React, Vue, Svelte, HTML 렌더러
- 렌더링 경로 2개:
bindForm,createForm - 시나리오 보고서의 전송 2개: 네이티브 multipart 폼, ordered JSON
모든 보고서는 동일한 명세, 컴파일한 템플릿, 데이터, 검사를 사용합니다. 시나리오
검사는 ssr 프레임에서 실행합니다. 네이티브
저장과 JSON 저장은 동일한 레코드, 식별자, 부모 식별자, 위치를 생성해야 합니다.
Ordered JSON은 모든 깊이의 객체 멤버 순서를 보존합니다.
구조, 데이터, 식별자
구조 컴파일, 인스턴스 데이터, 렌더링, 검증을 분리합니다. 컴파일한 템플릿은 레코드 값을 포함하지 않고 JSON으로 직렬화할 수 있으며 컴파일을 사용할 수 없어진 후에도 재사용합니다. 동일한 캐시 키의 반복 데이터 주입, 편집, 재마운트는 컴파일 요청을 추가하지 않습니다.
반복 컬렉션은 행 식별자를 키로 사용하는 객체입니다. 저장된 sequence는 __와
13자리 십진수와 __로 구성합니다. 저장 전 신규 행은 __와 13자리 소문자
16진수와 __로 구성합니다. 키는 부모 컬렉션 안의 행을 식별하며 객체 멤버
순서가 표시 순서를 결정합니다. 런타임은 키 문자열로 저장 여부를 추론하지 않습니다.
전송 데이터에는 숨김 sequence 컨트롤, 보조 식별자 필드, 순서 필드가 없습니다. 서버는 같은 부모 아래 저장된 행에서만 기존 키를 확인합니다. 확인되지 않은 키에는 새 sequence를 할당하고 범위가 명시된 키 변경을 반환합니다. 삭제된 sequence는 다시 사용하지 않습니다.
명시적인 빈 객체는 빈 컬렉션이며 행 컨트롤을 생성하지 않습니다. 컬렉션 데이터가 누락되면 초기 행 하나를 생성합니다. 표시된 빈 컬렉션은 추가 버튼을 제공합니다. 표시 상태 변경은 데이터를 생성, 제거, 전송하지 않습니다. 행이 있는 숨김 컬렉션은 해당 행을 계속 전송합니다.
브라우저 검사
각 시나리오 보고서는 다음 동작을 모두 검사합니다.
- 렌더링, 네이티브 이름, 숨김 식별자 컨트롤 0개
- 추가, 복사, 이동, 제거, 저장된 키 교체
- 5, 7, 1 순서의 비연속 저장 식별자
- 숨김 필수 필드를 포함한 클라이언트와 서버 검증
- 네이티브와 JSON 전송, 잘못된 요청 거부, 정확한 재조회
- 빈 회사, 스토어, 부서 컬렉션 전체 동작
- 부모 관계, 형제 식별자 유지, 삭제 ID 미사용
- 캐시 구조 재사용과 컴파일 요청 수 유지
초기화 보고서는 왼쪽 프레임에서 18단계를 실행하고 저장소를 초기화한 뒤 오른쪽
프레임에서 같은 단계를 실행합니다. 두 실행 모두 copied부터 saved-new까지 행 키
입력을 반복하므로 두 열이 같은 키를 생성합니다. 단계는 다음과 같습니다.
mounted: 저장소를 초기화하고 해당 열의 프레임 문서를 다시 불러 각자의 초기화 경로로 시작합니다. 서버가 렌더링한 폼을 하이드레이션(왼쪽)하거나 마운트 후 주입(오른쪽)reinjected-1,reinjected-2: 같은 캐시 템플릿으로 저장 레코드를 다시 주입data-hidden,data-restored: 조건 필드를 숨기는 레코드, 그다음 저장 레코드를 주입edited: 회사 이름, 스토어 이름, 메모를 수정하고 체크박스를 전환saved: 저장하고 검증 결과, 저장 레코드, 키 변경을 기록reloaded: 저장 레코드를 다시 조회copied,moved,copy-removed,added: 버튼으로 회사 행을 복사, 이동, 제거, 추가하고 추가한 행을 수정saved-new: 추가한 행을 저장collapsed-all,expanded-all,undone: 구조 맵의 모두 접기, 모두 펼치기, 되돌리기 버튼을 누름empty,restored: 빈 회사 컬렉션, 그다음 저장 레코드를 주입
단계는 키보드 사용자처럼 포커스를 표시해 컨트롤에 포커스합니다(focus({ focusVisible: true })).
브라우저는 같은 문서에 포인터 입력이 있었으면 스크립트 포커스를 표시하지 않으므로, 그러지 않으면
비교 전에 한 프레임 안을 클릭한 것만으로 그 열의 CSS만 달라집니다.
두 열은 같은 입력을 받아야 하는데 포인터는 한 프레임 위에만 있을 수 있습니다. 모든 브라우저에서
프레임 안의 :hover를 막는 페이지 스타일은 없습니다. Safari에서는 프레임의 pointer-events: none도,
프레임을 덮는 요소도 막지 못합니다. 따라서 캡처는 포인터가 두 프레임 밖에 있을 때만 합니다. 포인터가
프레임 위에 있으면(그 문서 요소가 :hover와 일치하면) 비교는 결과 없이 멈추고 포인터를 프레임 밖으로
옮긴 뒤 다시 비교하라고 알립니다. 로드 뒤 표시하는 목록도 비교하지 않고 같은 안내를 표시합니다.
페이지 자체는 스크롤하지 않으므로 멈춰 있는 포인터와 프레임의 위치 관계가 바뀌지 않습니다. 컨트롤과
결과는 뷰포트 높이의 45%로 제한한 패널 안에서 스크롤하고, 두 프레임은 나머지를 좌우로 나눠(1000px
이하에서는 위아래 절반) 각자 안에서 스크롤합니다. 따라서 런타임의 포커스 이동은 그 프레임만
스크롤합니다. 스크롤되는 페이지에 프레임을 세로로 쌓았을 때 Safari는 copied 단계의 포커스 이동에서
페이지를 0에서 602px로 스크롤해, 페이지 머리글에 멈춰 있던 포인터 아래로 왼쪽 프레임을 가져왔습니다.
오른쪽 각 단계는 저장한 왼쪽 단계와 formSnapshot, styleSnapshot,
compareSnapshots로 비교합니다. 비교 대상은 모든 속성을 포함한 파싱 DOM, 현재 컨트롤
상태, 네이티브 필드, 요소와 의사 요소의 계산된 CSS, ordered JSON 데이터, 포커스와 텍스트
선택 범위, 저장 응답입니다. 직렬화한 HTML은 비교하지 않습니다. 속성 순서는 DOM의 일부가
아니고, 브라우저 엔진은 프레임워크가 설정하는 속성을 서로 다른 순서로 만듭니다(Safari와
Chromium은 복원된 Vue checkbox의 checked와 value 순서가 다름). 각 열의 재주입·복원
단계는 그 열의 mounted 단계와도 비교합니다. 아무것도 제거, 교체, 정규화하지 않습니다.
보고서는 일곱 범주의 비교 24개, 결과 168개입니다.
페이지는 두 프레임을 모두 표시합니다. 두 프레임이 로드되면 상단 목록이 mounted
상태를 비교하고, 비교 버튼과 전체 검사는 단계가 끝날 때마다 결과를 추가합니다.
보고서는 단계마다 HTML, DOM, 컨트롤 상태, 필드, 데이터, 포커스, 응답, CSS digest와
실패한 CSS 비교의 기대·실제 CSS를 보존합니다.
두 프레임과 페이지, 같은 origin의 다른 모든 탭은 같은 저장 레코드를 읽고 교체합니다.
페이지에서 시작한 작업(프레임 버튼이나 폼 제출, 비교 버튼, 전체 검사)은 origin 잠금
하나(navigator.locks)를 가진 동안에만 레코드를 변경합니다. 다른 작업이 잠금을 가진 동안
시작한 작업은 실행되지 않고 다른 검사나 저장이 실행 중이라고 알립니다. 전체 검사나 검증기가
직접 호출하는 검사는 이미 잠금을 가진 작업 안에서 실행됩니다. 잠금은 브라우저 하나에 속합니다. 같은
서버를 쓰는 다른 브라우저, 다른 사람, 자동화 실행은 잠금 없이 같은 레코드를 읽고 교체하므로, 서로 다른
브라우저의 검사를 동시에 실행하면 안 됩니다.
포인터와 키보드 검사는 행 추가 후 포커스가 새 행의 첫 입력으로 이동하고, 그 입력이 프레임 뷰포트와 메인 페이지 뷰포트 안에 모두 보이는지 확인합니다. 프레임과 페이지는 필요한 만큼만 스크롤합니다. 프레임 문서 검사는 모든 렌더링 경로와 프레임워크의 초기화 문서 두 개를 확인합니다. CSR 문서는 뷰가 비어 있고 페이로드가 없는 빌드 문서 그대로여야 하고, SSR 문서는 같은 문서에 세 가지 삽입만 적용한 것이어야 하며, 네 서버 문서 뒤의 빌드 문서를 SHA-256으로 비교합니다. 생성 검사는 모든 서버와 프레임워크의 한국어·영어 SSR 출력을 확인하고, 모든 서버가 같은 잘못된 SSR 쿼리를 같은 메시지로 거부하는지 확인합니다.
실행기와 보고서
브라우저는 작업을 시작하고 호스트는 짧은 프로토콜 호출로 상태와 완료 보고서를 수집합니다. 장시간 행렬 전체를 하나의 DevTools 프로토콜 호출에서 실행하지 않습니다. 수집기는 현재 보고서, 보고서 시작 시각, 완료 보고서 수, 요청·응답 수, 마지막 요청·응답 시각을 기록합니다.
서버 실행의 각 단위는 가장 느린 측정값에서 정한 자기 제한 시간(measuredLimitMs)을
가지며, 실행 전체를 덮는 제한 시간은 없습니다. 측정값은 브라우저 검사 네 개를 동시에 실행한
2026-09-16 배포 검증에서 얻었습니다. 최대 32.4초로 측정한 초기화 보고서는 100,000밀리초,
2.2초로 측정한 시나리오 보고서는 10,000밀리초, 1.9초로 측정한 보고서 앞·사이·뒤의 페이지
작업은 10,000밀리초를 가집니다(src/browser-job.mjs의 browserReportMeasurementsMs). 보고서
작업을 둘러싼 단위도 자기 제한 시간을 가집니다(browser-report-policy.mjs의
browserUnitMeasurementsMs). 메인 페이지(1.8초)는 10,000밀리초이고, 상호작용 검사, 프레임 문서
검사, 산출물 기록은 합쳐서 26.3초가 걸렸으므로 각각을 따로 측정하기 전까지 각각 80,000밀리초입니다.
브라우저 시작과 종료는 같은 진행 줄을 출력하는 제한 시간 없는 장기 작업입니다(src/unit-pool.mjs의
runOperation). 서버 보고서는 이 단위들의 상태, 소요 시간, 제한 시간과 브라우저 시작·종료의 상태와
소요 시간을 units에 기록합니다. 단위는 node:test test로 실행되어 끝날 때 결과를 출력하고, 검사는 완료된 보고서마다 결과와 소요 시간을 출력합니다. 제한 시간에 도달한 단위는 그 단위 이름을 밝히는 node:test test를 실패시키며, 경과 시간은 units에 기록합니다. 실패는 현재 상태와 완료된 모든 보고서를 보존합니다. 브라우저 검사 전체는 제한 시간도, 합산 제한 시간도, 소요 시간 예산도 가지지
않습니다.
완료된 서버 보고서는 보고서 24개의 브라우저 작업을 요구합니다. 검사 19개씩의
시나리오 보고서 16개와 비교 결과 168개씩의 초기화 보고서 8개입니다. 또한 상호작용 검사
80개, csr 프레임의 로드 전 마운트 검사 8개, 프레임 문서 검사 16개, 브라우저·페이지
오류 0개, 보고서와 모든 시나리오·초기화 보고서가 공유하는 하나의 소스 식별자를 요구합니다.
보고서는 실행 시간을 기록하지만 실행 시간은 성공 조건이 아닙니다. passed 필드는 boolean이어야 합니다. 실행 활동, 초기화 단계, 시간
증거가 누락되면 보고서는 미완료입니다.
네 서버 집계는 서버마다 완료된 보고서 하나를 요구합니다. 시나리오 검사 1,216개,
초기화 비교 5,376개, 상호작용 검사 320개, 마운트 검사 32개, 프레임 문서 검사 64개가 모두
성공해야 하며, 네 서버 문서 뒤의 빌드 문서가 같아야 합니다. 실패, 누락,
잘못된 형식, 불일치가 하나라도 있으면 passed: false와 종료 상태 1을 반환합니다.
실패가 0개인 완전한 집계만 종료 상태 0을 반환합니다.
추가 검증
생성 검증은 성공 결과 450개, HTTP 요청 899개, 서버·렌더링 경로·프레임워크 조합 32개를 요구합니다. 저장소 검증은 원자적 갱신, 잠금, 위치 기반 조회, 부모 관계, 파일 변경 없는 요청 거부, 전체 삭제, sequence 할당을 확인합니다. 타입 검증은 모든 서버에서 동일한 스칼라와 컬렉션 규칙을 확인합니다.
PHP-FPM은 enable_post_data_reading=0으로 실행하므로 PHP 서버는 다른 서버처럼 요청 본문을 직접
읽습니다. urlencoded와 multipart 본문을 읽는 파서 하나가 반복된 필드, 값이면서 그룹인 이름, 잘못된
이름, 파일 파트를 400으로 거부합니다. 저장 검사의 request-size-limit과 레코드 저장소 사례
save-stops-reading-an-oversized-request는 PHP가 실행되기 전에 nginx에서 413을 받고, 브라우저 shape 검사의 필드 10,001개 네이티브 폼은 추가 필드로 400을 받습니다.
빠른 소스 검사는 보고서 정책 실패, 프로토콜 시간 제한 동작, 보고서별 제한 시간과 진행
보고, 단계 실행기의 끝까지 실행과 프로세스 트리 정리, 소스 식별자, 스냅샷 차이, 생성 캐시 동작,
요청 수 변경, 단위 실행기의 제한 시간과 진행 보고, 레코드 리소스의 고정 데이터와 링크, 정본 페이지가
선언한 컨트롤을 재현합니다. 이 검사는 로컬 검증을 대체하지 않습니다. main push CI는
npm run test:form-comparison을 실행해 보고서 정책, 브라우저 작업, 소스 트리와 PHP 서버(php 모드의
servers/php PHPUnit suite) 회귀가 있으면 통합을 실패로 처리하고, npm run test:form-comparison:pipeline을 실행해 다섯
서버의 레코드 저장소 계약, Go·Rust 서버 테스트, 40개 조합의 정본 흐름 검사가 실패하면 통합을
실패로 처리하며, npm run test:form-comparison:checks를 실행해 아래 로컬 검증이 실패하면 통합을
실패로 처리합니다.
로컬 검증
examples/form-comparison/local-verification.mjs(make crudui-test-form-comparison-checks)는 정본 흐름 검사처럼
체크아웃에서 로컬 구성 하나를 빌드합니다. 레코드 서버
프로그램, 공개 디렉터리, 네이티브 레코드 서버 네 개, 공개 서버이며, 각각 시스템이 정한 127.0.0.1의 port에서
로컬 프로세스로 실행합니다. 결과 디렉터리의 source.json에 식별자를 기록한 뒤 다음 단계를 순서대로
실행하며 실패한 단계가 있는 첫 단계에서 멈춥니다.
- php 모드의 PHP 서버(
servers/php) PHPUnit suite. 그 groupmodules는 이 실행이 빌드한polyspec_crudui.so와ordered_json.so를 로드하는 child process에서 처리 모드를 실행합니다 - php-ext 모드의 같은 suite. 그 PHP는 kit run-tests의 반복 가능한
--php-extension(K165)으로 이 실행의 두 모듈을 로드하며, PHP 라이브러리를 구성하는 groupphp는 빼고 실행합니다 - 공개 서버를 거쳐 네이티브 서버 네 개를 대상으로 하는 생성 검사(
check-generation.mjs) - 네이티브 서버 네 개를 대상으로 구성의 데이터 디렉터리 파일을 읽는 저장 검사(
check-servers.mjs) - 구성 페이지의 40개 조합 정본 흐름 검사(
check-pipeline.mjs, 단위마다 자기 제한 시간) - PHP, PHP 확장, Go, Rust 브라우저 검증 네 개를 동시에(
check.mjs와check-interaction.mjs의 상호작용 검사, 단위마다 자기 제한 시간) - 이번 실행의 브라우저 보고서 네 개를 집계(
check-browser-reports.mjs) - 모든 프레임워크와 렌더링 경로의 키 간격 0, 10, 50밀리초 입력 검사(
check-typing.mjs). 프레임의 PHP 레코드를 수정하므로 브라우저 검사 뒤에 실행합니다.
각 단계의 step은 시간 한도 없이 끝까지 실행하며 진행을 출력합니다. 브라우저 검사 네 개를 동시에 실행하는 이유는 각 검사가 자기 브라우저 프로세스에서 자기 포커스, 선택, 스크롤을 측정하고 자기 서버와 자기 저장 레코드를 사용하기 때문입니다.
이후 체크아웃의 식별자가 바뀌지 않았는지 확인합니다. 근거 검사(verification-evidence.mjs)는 생성 보고서,
저장 보고서, 정본 흐름 보고서(pipeline.json: 성공한 조합 40개와 성공한 저장소 초기화 5개), 브라우저 집계,
입력 보고서(모든 프레임워크, 렌더링 경로, 간격이 성공하고 페이지 오류가 없음)가 이 계약의 모든 개수와 성공
조건을 충족하고 보고서가 그 식별자를 명시하도록 요구합니다. 서버는 실패한 경우에도 실행이 끝날 때
멈춥니다. 실패한 실행은 결과 디렉터리를 남기고 그 경로를 출력합니다.
CI는 같은 검사를 두 부분으로 나누어 각각 한 번 실행합니다. CI 작업 form-comparison-browser는 네이티브
서버(php, php-ext, go, rust)마다 작업 하나를 두므로 각 브라우저 검사가 runner를 혼자 씁니다. 그
서버를 FORM_SERVERS로 지정한 make crudui-test-form-comparison-browser가 실행기를 --servers로 실행하며, 실행기는
로컬 구성을 시작하고 그 서버의 브라우저 검사(5단계)만 실행하고, 작업은 그 서버의 보고서를 올립니다. CI 작업
form-comparison-checks는 네 작업 뒤에 실행되어 보고서를 내려받고 make crudui-test-form-comparison-summary를
실행하며, 이는 실행기를 --browser-reports로 실행합니다. 실행기는 자기 로컬 구성을 시작하고 나머지 단계(1~4,
6, 7)와 근거 검사를 실행하며, 브라우저 집계는 내려받은 보고서 네 개를 읽습니다. 이 실행들의 모든 서버는
system이 정한 port를 쓰므로 동시에 실행해도 충돌하지 않습니다. 집계는 모든 보고서가 자기 origin의 scheme과
host, 그리고 같은 트리의 소스 식별자를 명시하도록 요구하며, port는 실행마다 다릅니다. 검사, 보고서, 제한 시간은
단일 실행과 같습니다.