diff options
| -rw-r--r-- | docs/design/001-first-component.md | 26 | ||||
| -rw-r--r-- | src/main.c | 21 | ||||
| -rw-r--r-- | src/screen.c | 56 | ||||
| -rw-r--r-- | src/vega.h | 5 | ||||
| -rw-r--r-- | tests/check.py | 59 |
5 files changed, 163 insertions, 4 deletions
diff --git a/docs/design/001-first-component.md b/docs/design/001-first-component.md index db04bad..5033389 100644 --- a/docs/design/001-first-component.md +++ b/docs/design/001-first-component.md @@ -46,6 +46,29 @@ Tab이 키 모드로 바꾸면 글자·숫자·Space가 `KeyA`·`Digit0`·`Space 표시 폭은 Vega가 맡는다. 한글·CJK·그림 이모지는 두 칸, 결합 문자·변이 선택자·ZWJ는 0칸, 제어 문자는 제어 그림(U+2400대)으로 보인다. grapheme 단위 자르기는 아직 없어 ZWJ 이모지 묶음은 근사다. +## 규약 검사 (`vega check`) + +`vega check app.pmod`는 모듈이 뜻한 듯하지만 규약에 맞지 않는 곳을 알린다. 표면은 이런 보기를 조용히 다른 +부품으로 읽는다. 검사는 그 읽기를 드러내어 오타나 타입 실수가 화면에서 사라지지 않게 한다. 찾으면 종료 코드 1이다. +규칙은 화면을 읽는 코드와 같은 파일(`src/screen.c`)에 있다. + +| 찾는 것 | 표면이 대신 하는 일 | +|---|---| +| 수인 `title` | 필드로 보인다 | +| 수인 `label` 열 | 목록이 아니라 표로 보인다 | +| text인 `tone` 열 | 목록은 무시, 표는 열로 보인다 | +| 수인 `badge`·`detail` 열 | 보이지 않는다 | +| 목록의 그 밖의 열 | 보이지 않는다(목록은 label, badge, detail, tone만 보인다) | +| 보일 열이 없는 표 | 제목과 행 수만 보인다 | +| 목록·표가 없는 `X_focus`, text인 `_focus` | 필드로 보이거나 포커스가 없다 | +| 두 번째 text 입력 | 입력할 곳이 없다(첫 번째만 입력 줄을 얻는다) | + +```text +$ vega check bad.pmod +bad.pmod: items_list.tone: a tone is a number (vega.normal ... vega.danger); the list ignores it +bad.pmod: missing_focus: names no list or table missing; it shows as a field +``` + ## 영속 모든 표면은 `--store 경로`를 받는다. 앱의 상태는 Pure가 그 트리 저장소에 두므로, 터미널에서 적은 할 일을 다음 실행의 @@ -64,10 +87,11 @@ CLI나 문장 표면이 그대로 이어받는다. Vega에도 앱에도 저장 � - 한 저장소를 CLI와 문장 표면이 번갈아 이어 쓰며(시드마다 7번) 매번 모델과 같은 목록을 보이고, 대화형 세션에서 입력한 것이 다음 실행에 남는다. - aup와 Vega의 예제 11개를 세 표면에 무작위 입력으로 돌려 비정상 종료가 없다. sanitizer 빌드로도 같은 검사를 통과한다. +- 규약을 여덟 가지로 어긴 명세에서 `vega check`가 아홉 곳을 모두 정확한 문장으로 알리고, 예제에서는 아무것도 알리지 + 않는다. ## 남은 것 -- 규약 오류를 조용히 넘기지 않기: `label`이 수인 목록, 표에 없는 `_focus` 같은 것을 `vega check`로 알린다. - 포커스를 표면에서 바꾸기(GUI 클릭, TUI 마우스). 지금은 앱의 키 핸들러만 포커스를 옮긴다. Pure에 표의 인덱스를 이벤트로 바꾸는 호스트 API가 필요하다. - 셀 단위 갱신(TUI는 매 변경마다 전체를 다시 그린다), GUI 표면, 음성 합성으로 가는 NUI. @@ -11,6 +11,7 @@ #include <unistd.h> /* vega SURFACE MODULE [--store PATH]: one module, one screen, several surfaces. + * check what the module seems to mean but does not meet (exit 1) * tui interactive terminal * tui --dump WxH the terminal's frames as text, driven by a script * cli plain lines for pipes, driven by a script @@ -29,7 +30,7 @@ static char entry[VEGA_TEXT]; static size_t entry_length; static bool typing; -static bool load(const char *path, const char *store) { +static bool load_module(const char *path) { static unsigned char bytes[1 << 17]; FILE *file = fopen(path, "rb"); size_t size = file ? fread(bytes, 1, sizeof(bytes), file) : 0; @@ -41,6 +42,15 @@ static bool load(const char *path, const char *store) { fprintf(stderr, "vega: %s: %s\n", path, error); return false; } + return true; +} + +static void print_finding(void *path, const char *name, const char *message) { + printf("%s: %s: %s\n", (const char *)path, name, message); +} + +static bool load(const char *path, const char *store) { + if (!load_module(path)) return false; if (store && pure_store_init(store) != PURE_OK) { fprintf(stderr, "vega: cannot open store %s\n", store); return false; } PureResult started = pure_app_start(&app, &module, store ? pure_tree_data(store) : (PureData){0}); if (started != PURE_OK) { fprintf(stderr, "vega: cannot start (%d)\n", (int)started); return false; } @@ -376,11 +386,16 @@ int main(int argc, char **argv) { width <= 1000 && height <= 1000) dumped = true; else usage = true; } - if (usage || (dumped && strcmp(argv[1], "tui"))) { + if (usage || (dumped && strcmp(argv[1], "tui")) || (!strcmp(argv[1], "check") && argc != 3)) { fputs("vega tui|cli|say MODULE.pmod [--store PATH]\n" - "vega tui MODULE.pmod [--store PATH] --dump WIDTHxHEIGHT < script\n", stderr); + "vega tui MODULE.pmod [--store PATH] --dump WIDTHxHEIGHT < script\n" + "vega check MODULE.pmod\n", stderr); return 2; } + if (!strcmp(argv[1], "check")) { + if (!load_module(argv[2])) return 2; + return vega_check(&module, print_finding, argv[2]) ? 1 : 0; + } Surface surface = !strcmp(argv[1], "cli") ? SURFACE_CLI : !strcmp(argv[1], "say") ? SURFACE_SAY : !strcmp(argv[1], "tui") ? SURFACE_TUI : (Surface)-1; if ((int)surface < 0) { fprintf(stderr, "vega: unknown surface %s\n", argv[1]); return 2; } diff --git a/src/screen.c b/src/screen.c index 8e584a5..2d4c6dc 100644 --- a/src/screen.c +++ b/src/screen.c @@ -50,6 +50,62 @@ void vega_screen(VegaScreen *screen, const PureModule *m) { } } +static bool is_view(const PureModule *m, int view, uint8_t type) { + return view >= 0 && m->view_type[view] == type; +} + +unsigned vega_check(const PureModule *m, void (*report)(void *, const char *name, const char *message), void *context) { + unsigned found = 0; + char name[2 * PURE_MODULE_NAME + 8], message[256]; +#define FIND(...) do { snprintf(message, sizeof(message), __VA_ARGS__); report(context, name, message); ++found; } while (0) + int title = pure_module_view(m, "title"); + snprintf(name, sizeof(name), "title"); + if (title >= 0 && m->view_type[title] != PURE_VIEW_TEXT) FIND("a title is text; this one shows as a field"); + for (uint32_t i = 0; i < m->views; ++i) { + if (m->view_table[i]) continue; + const char *view = m->view_names[i]; + size_t length = strlen(view); + snprintf(name, sizeof(name), "%s", view); + if (length > 6 && !strcmp(view + length - 6, "_focus")) { + char table[PURE_MODULE_NAME]; + snprintf(table, sizeof(table), "%.*s", (int)(length - 6), view); + int owner = pure_module_view(m, table); + if (!is_view(m, owner, PURE_VIEW_TABLE)) + FIND("names no list or table %s; it shows as a field", table); + else if (m->view_type[i] != PURE_VIEW_NUM) FIND("the focused row is a number; %s shows no focus", table); + } + if (m->view_type[i] != PURE_VIEW_TABLE) continue; + int label = pure_module_column(m, i, "label"), tone = pure_module_column(m, i, "tone"); + bool list = is_view(m, label, PURE_VIEW_TEXT); + unsigned shown = 0; + for (uint32_t k = 0; k < m->views; ++k) { + if (m->view_table[k] != i + 1) continue; + const char *column = m->view_names[k]; + bool text = m->view_type[k] == PURE_VIEW_TEXT; + snprintf(name, sizeof(name), "%s.%s", view, column); + if ((int)k == label && !text) FIND("a list's label is text; %s shows as a table", view); + else if ((int)k == tone && text) FIND("a tone is a number (vega.normal ... vega.danger); %s", list ? + "the list ignores it" : "it shows as a column"); + else if (list && (!strcmp(column, "badge") || !strcmp(column, "detail")) && !text) + FIND("a list's %s is text; this one is not shown", column); + else if (list && (int)k != label && (int)k != tone && strcmp(column, "badge") && strcmp(column, "detail")) + FIND("a list shows label, badge, detail and tone; this column is not shown"); + shown += (int)k != tone || text; + } + snprintf(name, sizeof(name), "%s", view); + if (!list && !shown) FIND("a table with no column to show"); + } + int prompt = -1; + for (uint32_t i = 0; i < m->inputs; ++i) { + if (m->input[i].kind != PURE_KIND_TEXT) continue; + snprintf(name, sizeof(name), "%s", m->input_names[i]); + if (prompt >= 0) FIND("only the first text input (%s) has a place to type into", m->input_names[prompt]); + else prompt = (int)i; + } +#undef FIND + return found; +} + typedef struct { char *out; size_t used; } Collect; static bool collect(void *context, PureSpan span) { @@ -34,6 +34,11 @@ typedef struct { } VegaScreen; void vega_screen(VegaScreen *, const PureModule *); +/* What a module seems to mean but does not meet: a list label that is not + * text, a _focus view without its table, a column a list never shows, a + * second text input. Each finding names the view or input it is about; + * returns how many there were. */ +unsigned vega_check(const PureModule *, void (*report)(void *, const char *name, const char *message), void *context); /* A view (row ignored) or a column at a row as text; numbers in Pure's * shortest form. false when it has no value (for example a missing item). */ diff --git a/tests/check.py b/tests/check.py index 6256023..8b86c71 100644 --- a/tests/check.py +++ b/tests/check.py @@ -16,6 +16,8 @@ 4. Every aup example runs on every surface under random keys without a crash. 5. Stored: surfaces take turns on one store and each continues the list; an interactive session's entries are there in the next one. +6. vega check names every convention a spec seems to mean but misses, and + none in the examples. """ import argparse import json @@ -304,6 +306,61 @@ def interactive(vega, module, store=None): return 1 +BROKEN_SPEC = """use vega +record Item { + name: text + size = 0 +} +state items: list of Item +state at: index of items +input add = text +input note = text +input up = key ArrowUp +on add { items.append(Item { name: add }) } +on note { items.append(Item { name: note }) } +on up.down { at = at - 1 } +view title = 3 +view items_list = each item in items { + label = item.name + tone = "red" + badge = item.size + size = item.size +} +view sizes = each item in items { + label = item.size +} +view empty = each item in items { + tone = vega.muted +} +view missing_focus = 1 +view items_list_focus = "0" +""" + + +def conventions(vega, aup, work, modules): + """Every convention the broken spec misses, and none in the examples.""" + env = dict(os.environ, AUP_PATH=str(ROOT / 'lib')) + (work / 'broken.aup').write_text(BROKEN_SPEC) + built = run(aup, 'build', work / 'broken.aup', '-o', work / 'broken.pmod', env=env) + assert built.returncode == 0, built.stderr + result = run(vega, 'check', work / 'broken.pmod') + found = [line.split(': ', 1)[1] for line in result.stdout.splitlines()] + assert result.returncode == 1 and found == [ + 'title: a title is text; this one shows as a field', + 'items_list.tone: a tone is a number (vega.normal ... vega.danger); the list ignores it', + "items_list.badge: a list's badge is text; this one is not shown", + 'items_list.size: a list shows label, badge, detail and tone; this column is not shown', + "sizes.label: a list's label is text; sizes shows as a table", + 'empty: a table with no column to show', + 'missing_focus: names no list or table missing; it shows as a field', + 'items_list_focus: the focused row is a number; items_list shows no focus', + 'note: only the first text input (add) has a place to type into'], result.stdout + for module in modules.values(): + clean = run(vega, 'check', module) + assert clean.returncode == 0 and not clean.stdout, clean.stdout + return len(found) + + def robustness(vega, aup, work, rounds): """Every aup example, every surface, random keys and text: never a crash.""" examples = sorted((ROOT.parent / 'aup' / 'examples').glob('*.aup')) + sorted((ROOT / 'examples').glob('*.aup')) @@ -315,6 +372,7 @@ def robustness(vega, aup, work, rounds): module = work / (path.stem + '.pmod') built = run(aup, 'build', path, '-o', module, env=env) assert built.returncode == 0, (path, built.stderr) + assert run(vega, 'check', module).returncode in (0, 1), path rng = random.Random(path.stem) for _ in range(rounds): script = [] @@ -357,6 +415,7 @@ def main(): report['interactive_sessions'] = interactive(vega, modules['todo']) + \ interactive(vega, modules['todo'], work / 'interactive-store') report['stored_surface_lines'] = stored_surfaces(vega, modules['todo'], work, range(args.seeds)) + report['convention_findings'] = conventions(vega, aup, work, modules) report['examples'], report['robustness_runs'] = robustness(vega, aup, work, args.rounds) print(json.dumps(report)) |
