refactor: remove app artifact workflow
This commit is contained in:
parent
296eca8a59
commit
238b58956e
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
*.elc
|
*.elc
|
||||||
tests/*.elc
|
tests/*.elc
|
||||||
.etaf-build/
|
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -53,7 +53,7 @@ perf-evaluator: compile
|
|||||||
|
|
||||||
# Run package regressions in dependency order. Keeping them in one recipe
|
# Run package regressions in dependency order. Keeping them in one recipe
|
||||||
# prevents parallel make from cleaning/recompiling a sibling while another
|
# prevents parallel make from cleaning/recompiling a sibling while another
|
||||||
# package is loading its artifacts.
|
# package is loading its compiled dependencies.
|
||||||
perf-regressions:
|
perf-regressions:
|
||||||
$(MAKE) -C ../ecss test EMACS="$(EMACS)"
|
$(MAKE) -C ../ecss test EMACS="$(EMACS)"
|
||||||
$(MAKE) -C ../tp test EMACS="$(EMACS)"
|
$(MAKE) -C ../tp test EMACS="$(EMACS)"
|
||||||
|
|||||||
@ -19,13 +19,6 @@ into the right-hand preview. Saving a source file also refreshes by default.
|
|||||||
`etaf-playground-register-example` is available when a companion needs a
|
`etaf-playground-register-example` is available when a companion needs a
|
||||||
non-conventional root or feature name.
|
non-conventional root or feature name.
|
||||||
|
|
||||||
ETAF core provides the generic `M-x etaf-compile-app` and
|
|
||||||
`M-x etaf-app-compile-status` commands; see the [ETAF
|
|
||||||
documentation](../etaf/README.md) for the full contract. Playground does not
|
|
||||||
implement compilation. It registers discovered examples as ETAF Apps and adds
|
|
||||||
a `Compile App` shortcut plus status to the header line. Its default artifact
|
|
||||||
directory is `.etaf-build/`.
|
|
||||||
|
|
||||||
The low-level `etaf-playground-mount-example` API remains available for batch
|
The low-level `etaf-playground-mount-example` API remains available for batch
|
||||||
tests and consumers that only need a preview buffer. Business Components,
|
tests and consumers that only need a preview buffer. Business Components,
|
||||||
database schemas, palettes, refs, and handlers stay in the example companion.
|
database schemas, palettes, refs, and handlers stay in the example companion.
|
||||||
|
|||||||
@ -18,11 +18,6 @@ ETAF Playground 是通用的应用构建工作区:左侧编辑同一个应用
|
|||||||
也会刷新。若 root 或 feature 不遵循命名约定,可在 companion 中调用
|
也会刷新。若 root 或 feature 不遵循命名约定,可在 companion 中调用
|
||||||
`etaf-playground-register-example` 注册覆盖。
|
`etaf-playground-register-example` 注册覆盖。
|
||||||
|
|
||||||
ETAF core 提供通用 App 预编译命令 `M-x etaf-compile-app` 和状态命令
|
|
||||||
`M-x etaf-app-compile-status`;完整合同见 [ETAF 文档](../etaf/README.zh-CN.md)。
|
|
||||||
Playground 不实现编译,只把发现的 example 注册成 ETAF App,并在 header line
|
|
||||||
显示 `Compile App` 快捷按钮和当前状态。默认产物目录是 `.etaf-build/`。
|
|
||||||
|
|
||||||
`etaf-playground-mount-example` 仍作为低层 batch/consumer API 保留。业务
|
`etaf-playground-mount-example` 仍作为低层 batch/consumer API 保留。业务
|
||||||
Component、数据库 schema、palette、refs 和 handlers 都应该留在 example companion
|
Component、数据库 schema、palette、refs 和 handlers 都应该留在 example companion
|
||||||
中;Playground 只提供 source/preview 会话、读文件、窗口切换与生命周期。
|
中;Playground 只提供 source/preview 会话、读文件、窗口切换与生命周期。
|
||||||
|
|||||||
@ -50,13 +50,6 @@
|
|||||||
:type 'directory
|
:type 'directory
|
||||||
:group 'etaf-playground)
|
:group 'etaf-playground)
|
||||||
|
|
||||||
(defcustom etaf-playground-artifact-directory
|
|
||||||
(expand-file-name ".etaf-build"
|
|
||||||
(file-name-directory (or load-file-name buffer-file-name)))
|
|
||||||
"Directory containing manually compiled `.etafc' App artifacts."
|
|
||||||
:type 'directory
|
|
||||||
:group 'etaf-playground)
|
|
||||||
|
|
||||||
(defcustom etaf-playground-source-extensions
|
(defcustom etaf-playground-source-extensions
|
||||||
'(".etaf" ".el" ".ecss")
|
'(".etaf" ".el" ".ecss")
|
||||||
"Source extensions shown in a Playground workspace, in tab order."
|
"Source extensions shown in a Playground workspace, in tab order."
|
||||||
@ -208,15 +201,6 @@ the root factory."
|
|||||||
(specs (mapcar (lambda (name)
|
(specs (mapcar (lambda (name)
|
||||||
(etaf-playground--example-spec name directory))
|
(etaf-playground--example-spec name directory))
|
||||||
(sort (delete-dups names) #'string<))))
|
(sort (delete-dups names) #'string<))))
|
||||||
(dolist (spec specs)
|
|
||||||
(etaf-register-app
|
|
||||||
(plist-get spec :pair)
|
|
||||||
:source (plist-get spec :el-file)
|
|
||||||
:static (plist-get spec :etaf-file)
|
|
||||||
:style (plist-get spec :ecss-file)
|
|
||||||
:artifact
|
|
||||||
(expand-file-name (concat (plist-get spec :pair) ".etafc")
|
|
||||||
etaf-playground-artifact-directory)))
|
|
||||||
(setq etaf-playground-scenario-manifest specs
|
(setq etaf-playground-scenario-manifest specs
|
||||||
etaf-playground-example-names (mapcar (lambda (spec)
|
etaf-playground-example-names (mapcar (lambda (spec)
|
||||||
(plist-get spec :pair))
|
(plist-get spec :pair))
|
||||||
@ -452,11 +436,6 @@ One-argument roots remain supported for small existing examples."
|
|||||||
(wrong-number-of-arguments
|
(wrong-number-of-arguments
|
||||||
(funcall root static-form)))))
|
(funcall root static-form)))))
|
||||||
|
|
||||||
(defun etaf-playground--compiled-artifact (spec session)
|
|
||||||
"Return SPEC's validated compiled artifact, unless SESSION owns sources."
|
|
||||||
(unless session
|
|
||||||
(etaf-app-load-artifact-or-warn (plist-get spec :pair))))
|
|
||||||
|
|
||||||
(defun etaf-playground-read-pair (name &optional session)
|
(defun etaf-playground-read-pair (name &optional session)
|
||||||
"Load NAME's companion and return its root View value.
|
"Load NAME's companion and return its root View value.
|
||||||
|
|
||||||
@ -464,13 +443,8 @@ When SESSION is supplied, its current source buffers are authoritative. The
|
|||||||
`.etaf' and `.ecss' files are read as inert data. The `.el' companion is the
|
`.etaf' and `.ecss' files are read as inert data. The `.el' companion is the
|
||||||
only file evaluated by the framework."
|
only file evaluated by the framework."
|
||||||
(let* ((spec (etaf-playground-scenario name))
|
(let* ((spec (etaf-playground-scenario name))
|
||||||
(artifact (etaf-playground--compiled-artifact spec session))
|
(static-form (etaf-playground-read-static name session))
|
||||||
(static-form (if artifact
|
(ecss-form (etaf-playground-read-ecss name session)))
|
||||||
(plist-get artifact :static-form)
|
|
||||||
(etaf-playground-read-static name session)))
|
|
||||||
(ecss-form (if artifact
|
|
||||||
(or (plist-get artifact :style-form) '(styles))
|
|
||||||
(etaf-playground-read-ecss name session))))
|
|
||||||
(etaf-playground--load-companion spec session)
|
(etaf-playground--load-companion spec session)
|
||||||
(etaf-playground--call-root
|
(etaf-playground--call-root
|
||||||
(etaf-playground--root-factory spec)
|
(etaf-playground--root-factory spec)
|
||||||
@ -659,71 +633,8 @@ the preview instead of destroying the source workspace."
|
|||||||
'help-echo (format "Switch to %s" extension)))))
|
'help-echo (format "Switch to %s" extension)))))
|
||||||
etaf-playground-source-extensions
|
etaf-playground-source-extensions
|
||||||
" | ")
|
" | ")
|
||||||
" "
|
|
||||||
(let* ((name (etaf-playground-session-name session))
|
|
||||||
(dirty
|
|
||||||
(cl-some (lambda (entry)
|
|
||||||
(and (buffer-live-p (cdr entry))
|
|
||||||
(buffer-modified-p (cdr entry))))
|
|
||||||
(etaf-playground-session-source-buffers session)))
|
|
||||||
(status (unless dirty (etaf-app-compile-status name)))
|
|
||||||
(state (if dirty 'dirty (plist-get status :status))))
|
|
||||||
(concat
|
|
||||||
(make-text-button
|
|
||||||
" Compile App " nil
|
|
||||||
'action #'etaf-playground-compile-current-app
|
|
||||||
'follow-link t
|
|
||||||
'button-data name
|
|
||||||
'help-echo "Save sources and compile this App with ETAF")
|
|
||||||
" | "
|
|
||||||
(make-text-button
|
|
||||||
(format " %s " state) nil
|
|
||||||
'action #'etaf-playground-show-current-app-compile-status
|
|
||||||
'follow-link t
|
|
||||||
'button-data name
|
|
||||||
'help-echo "Show the generic ETAF App compilation status")))
|
|
||||||
" C-c C-c refresh"))))
|
" C-c C-c refresh"))))
|
||||||
|
|
||||||
(defun etaf-playground--button-app-name (button)
|
|
||||||
"Return App name carried by header BUTTON."
|
|
||||||
(or (and (stringp button)
|
|
||||||
(get-text-property 0 'button-data button))
|
|
||||||
(and (fboundp 'button-get)
|
|
||||||
(ignore-errors (button-get button 'button-data)))
|
|
||||||
etaf-playground-current-example
|
|
||||||
(and (etaf-playground-session-p etaf-playground-session)
|
|
||||||
(etaf-playground-session-name etaf-playground-session))))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(defun etaf-playground-compile-current-app (&optional button)
|
|
||||||
"Compile BUTTON's App, or the current App, through generic ETAF APIs."
|
|
||||||
(interactive)
|
|
||||||
(let* ((name (etaf-playground--button-app-name button))
|
|
||||||
(session (and name (gethash name etaf-playground--sessions))))
|
|
||||||
(unless name (user-error "No ETAF App is associated with this buffer"))
|
|
||||||
(when (and session
|
|
||||||
(cl-some (lambda (entry)
|
|
||||||
(and (buffer-live-p (cdr entry))
|
|
||||||
(buffer-modified-p (cdr entry))))
|
|
||||||
(etaf-playground-session-source-buffers session)))
|
|
||||||
(user-error "Save %s source buffers before compiling the App" name))
|
|
||||||
(let ((status (etaf-compile-app name)))
|
|
||||||
(when session (etaf-playground--refresh-source-headers session))
|
|
||||||
(message "Compiled ETAF App %s: %s"
|
|
||||||
name (plist-get status :file))
|
|
||||||
status)))
|
|
||||||
|
|
||||||
;;;###autoload
|
|
||||||
(defun etaf-playground-show-current-app-compile-status (&optional button)
|
|
||||||
"Show generic compile status for BUTTON's App or the current App."
|
|
||||||
(interactive)
|
|
||||||
(let* ((name (etaf-playground--button-app-name button))
|
|
||||||
(status (and name (etaf-app-compile-status name))))
|
|
||||||
(unless status (user-error "No ETAF App is associated with this buffer"))
|
|
||||||
(message "ETAF App %s: %s (%s)" name
|
|
||||||
(plist-get status :status) (plist-get status :file))
|
|
||||||
status))
|
|
||||||
|
|
||||||
(defun etaf-playground--activate-source-tab (button)
|
(defun etaf-playground--activate-source-tab (button)
|
||||||
"Activate source tab BUTTON from a header-line text button."
|
"Activate source tab BUTTON from a header-line text button."
|
||||||
(let ((extension
|
(let ((extension
|
||||||
@ -985,8 +896,7 @@ batch tests and callers which do not need editor windows."
|
|||||||
nil t)))
|
nil t)))
|
||||||
(if buffer-name
|
(if buffer-name
|
||||||
(etaf-playground--mount-for-display buffer-name name)
|
(etaf-playground--mount-for-display buffer-name name)
|
||||||
(let ((_artifact-status (etaf-app-load-artifact-or-warn name))
|
(let ((session (etaf-playground--make-session name)))
|
||||||
(session (etaf-playground--make-session name)))
|
|
||||||
(etaf-playground--show-session session)
|
(etaf-playground--show-session session)
|
||||||
(etaf-playground-refresh session)
|
(etaf-playground-refresh session)
|
||||||
(etaf-playground--refresh-source-headers session)
|
(etaf-playground--refresh-source-headers session)
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
(require 'seq)
|
(require 'seq)
|
||||||
(require 'etaf-playground)
|
(require 'etaf-playground)
|
||||||
(require 'etaf-performance)
|
(require 'etaf-performance)
|
||||||
|
(require 'ebox-native-reflow)
|
||||||
|
|
||||||
(defvar etaf-research-shelf-database-file)
|
(defvar etaf-research-shelf-database-file)
|
||||||
(defvar etaf-research-shelf-fixture-size)
|
(defvar etaf-research-shelf-fixture-size)
|
||||||
@ -129,14 +130,13 @@ never included in SAMPLES. ACTION's result is passed to VERIFY and CLEANUP."
|
|||||||
(> (file-attribute-size (file-attributes database)) 0))
|
(> (file-attribute-size (file-attributes database)) 0))
|
||||||
(error "Prepared SQLite fixture was not created: %s" database)))
|
(error "Prepared SQLite fixture was not created: %s" database)))
|
||||||
|
|
||||||
(defun etaf-performance-evaluator--verify-compiled-runtime (status)
|
(defun etaf-performance-evaluator--verify-runtime-accelerator ()
|
||||||
"Require compiled App STATUS to include its native runtime accelerator."
|
"Require the optional Ebox native runtime accelerator."
|
||||||
(unless (and (eq (plist-get status :status) 'current)
|
(unless (and (require 'ebox-native-reflow nil t)
|
||||||
(eq (plist-get status :runtime-accelerator) 'ready))
|
(ebox-native-reflow-layout-ready-p))
|
||||||
(error "Performance evaluator requires the compiled native runtime: %S"
|
(error "Performance evaluator requires the Ebox native runtime"))
|
||||||
status))
|
|
||||||
(princ "perf-runtime-accelerator=ready\n")
|
(princ "perf-runtime-accelerator=ready\n")
|
||||||
status)
|
t)
|
||||||
|
|
||||||
(defun etaf-performance-evaluator--verify-environment (environment)
|
(defun etaf-performance-evaluator--verify-environment (environment)
|
||||||
"Require ENVIRONMENT to be suitable for an absolute latency gate."
|
"Require ENVIRONMENT to be suitable for an absolute latency gate."
|
||||||
@ -350,12 +350,7 @@ never included in SAMPLES. ACTION's result is passed to VERIFY and CLEANUP."
|
|||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
(etaf-performance-evaluator--prepare-database database)
|
(etaf-performance-evaluator--prepare-database database)
|
||||||
;; The maintained product path is a manually compiled App. Compile
|
(etaf-performance-evaluator--verify-runtime-accelerator)
|
||||||
;; and activate its optional native runtime before latency samples;
|
|
||||||
;; compiler command latency remains visible through the generic
|
|
||||||
;; compiler operation when users profile that command itself.
|
|
||||||
(etaf-performance-evaluator--verify-compiled-runtime
|
|
||||||
(etaf-compile-app "research-shelf"))
|
|
||||||
;; Latency samples measure the product path without observer work.
|
;; Latency samples measure the product path without observer work.
|
||||||
;; Trace behavior and overhead have separate, explicit gates below.
|
;; Trace behavior and overhead have separate, explicit gates below.
|
||||||
(etaf-performance-mode -1)
|
(etaf-performance-mode -1)
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"../scripts/benchmark-research-shelf")
|
"../scripts/benchmark-research-shelf")
|
||||||
(declare-function etaf-performance-evaluator--latency-failures
|
(declare-function etaf-performance-evaluator--latency-failures
|
||||||
"../scripts/benchmark-research-shelf")
|
"../scripts/benchmark-research-shelf")
|
||||||
(declare-function etaf-performance-evaluator--verify-compiled-runtime
|
(declare-function etaf-performance-evaluator--verify-runtime-accelerator
|
||||||
"../scripts/benchmark-research-shelf")
|
"../scripts/benchmark-research-shelf")
|
||||||
(declare-function etaf-performance-evaluator--verify-environment
|
(declare-function etaf-performance-evaluator--verify-environment
|
||||||
"../scripts/benchmark-research-shelf")
|
"../scripts/benchmark-research-shelf")
|
||||||
@ -58,17 +58,16 @@
|
|||||||
'(("theme-toggle" :count 20 :min 80.0 :p50 84.0
|
'(("theme-toggle" :count 20 :min 80.0 :p50 84.0
|
||||||
:p95 91.0 :max 111.0)))))))
|
:p95 91.0 :max 111.0)))))))
|
||||||
|
|
||||||
(ert-deftest etaf-performance-evaluator-requires-native-compiled-runtime ()
|
(ert-deftest etaf-performance-evaluator-requires-native-runtime ()
|
||||||
"Never report fallback-renderer latency as the optimized product gate."
|
"Never report fallback-renderer latency as the optimized product gate."
|
||||||
(cl-letf (((symbol-function 'princ) #'ignore))
|
(cl-letf (((symbol-function 'princ) #'ignore)
|
||||||
(should
|
((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
(equal
|
(lambda () t)))
|
||||||
(etaf-performance-evaluator--verify-compiled-runtime
|
(should (etaf-performance-evaluator--verify-runtime-accelerator)))
|
||||||
'(:status current :runtime-accelerator ready))
|
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
|
||||||
'(:status current :runtime-accelerator ready))))
|
(lambda () nil)))
|
||||||
(should-error
|
(should-error
|
||||||
(etaf-performance-evaluator--verify-compiled-runtime
|
(etaf-performance-evaluator--verify-runtime-accelerator)))
|
||||||
'(:status current :runtime-accelerator unavailable)))
|
|
||||||
(let ((makefile (with-temp-buffer
|
(let ((makefile (with-temp-buffer
|
||||||
(insert-file-contents "Makefile")
|
(insert-file-contents "Makefile")
|
||||||
(buffer-string))))
|
(buffer-string))))
|
||||||
|
|||||||
@ -113,23 +113,6 @@ database and mounts a test buffer before running BODY."
|
|||||||
"$(MAKE) -C ../etaf-sqlite compile"))
|
"$(MAKE) -C ../etaf-sqlite compile"))
|
||||||
(should (string-match-p (regexp-quote dependency) makefile)))))
|
(should (string-match-p (regexp-quote dependency) makefile)))))
|
||||||
|
|
||||||
(ert-deftest etaf-playground-registers-app-for-interactive-compilation ()
|
|
||||||
"Discovered examples use the generic ETAF App compile/status commands."
|
|
||||||
(let ((directory (make-temp-file "etaf-playground-artifacts-" t)))
|
|
||||||
(unwind-protect
|
|
||||||
(let ((etaf-playground-artifact-directory directory))
|
|
||||||
(etaf-playground-refresh-examples)
|
|
||||||
(should (eq (plist-get (etaf-app-compile-status "research-shelf")
|
|
||||||
:status)
|
|
||||||
'missing))
|
|
||||||
(let ((compiled (etaf-compile-app "research-shelf")))
|
|
||||||
(should (eq (plist-get compiled :status) 'current))
|
|
||||||
(should (> (plist-get compiled :blueprints) 0))
|
|
||||||
(should (file-readable-p (plist-get compiled :file)))))
|
|
||||||
(etaf-playground-refresh-examples)
|
|
||||||
(when (file-directory-p directory)
|
|
||||||
(delete-directory directory t)))))
|
|
||||||
|
|
||||||
(ert-deftest etaf-playground-static-reader-is-inert-and-strict ()
|
(ert-deftest etaf-playground-static-reader-is-inert-and-strict ()
|
||||||
"Read pair structure as inert data and reject executable AST nodes."
|
"Read pair structure as inert data and reject executable AST nodes."
|
||||||
(let* ((form (etaf-playground-read-static "research-shelf"))
|
(let* ((form (etaf-playground-read-static "research-shelf"))
|
||||||
@ -212,8 +195,7 @@ database and mounts a test buffer before running BODY."
|
|||||||
(etaf-playground-session-preview-buffer
|
(etaf-playground-session-preview-buffer
|
||||||
etaf-playground-session))))
|
etaf-playground-session))))
|
||||||
(let* ((header (etaf-playground--source-header))
|
(let* ((header (etaf-playground--source-header))
|
||||||
(position (string-match " EL" header))
|
(position (string-match " EL" header)))
|
||||||
(compile-position (string-match "Compile App" header)))
|
|
||||||
(should position)
|
(should position)
|
||||||
(should (get-text-property position 'button header))
|
(should (get-text-property position 'button header))
|
||||||
(should (equal ".el"
|
(should (equal ".el"
|
||||||
@ -223,11 +205,7 @@ database and mounts a test buffer before running BODY."
|
|||||||
(should (equal ".el"
|
(should (equal ".el"
|
||||||
(etaf-playground-session-active-extension
|
(etaf-playground-session-active-extension
|
||||||
etaf-playground-session)))
|
etaf-playground-session)))
|
||||||
(should compile-position)
|
(should-not (string-match-p "Compile App" header)))))))
|
||||||
(should (get-text-property compile-position 'button header))
|
|
||||||
(should (equal "research-shelf"
|
|
||||||
(get-text-property compile-position
|
|
||||||
'button-data header))))))))
|
|
||||||
|
|
||||||
(ert-deftest etaf-playground-direct-etaf-c-c-c-opens-workspace ()
|
(ert-deftest etaf-playground-direct-etaf-c-c-c-opens-workspace ()
|
||||||
"The source render command opens a workspace for a directly opened `.etaf'."
|
"The source render command opens a workspace for a directly opened `.etaf'."
|
||||||
@ -301,8 +279,8 @@ database and mounts a test buffer before running BODY."
|
|||||||
(etaf-runtime-for-buffer
|
(etaf-runtime-for-buffer
|
||||||
(etaf-playground-session-preview-buffer session))))))))
|
(etaf-playground-session-preview-buffer session))))))))
|
||||||
|
|
||||||
(ert-deftest etaf-playground-root-uses-compiled-blueprint ()
|
(ert-deftest etaf-playground-root-uses-automatic-blueprint-lowering ()
|
||||||
"The real root must instantiate a mixed static/dynamic blueprint."
|
"The real root automatically lowers supported mixed View subtrees."
|
||||||
(etaf-playground-test--with-app (buffer database)
|
(etaf-playground-test--with-app (buffer database)
|
||||||
(etaf-compiler-clear-cache)
|
(etaf-compiler-clear-cache)
|
||||||
(let* ((before (etaf-compiler-statistics))
|
(let* ((before (etaf-compiler-statistics))
|
||||||
@ -313,7 +291,7 @@ database and mounts a test buffer before running BODY."
|
|||||||
(let* ((after (etaf-compiler-statistics))
|
(let* ((after (etaf-compiler-statistics))
|
||||||
(blueprint (plist-get after :last-blueprint)))
|
(blueprint (plist-get after :last-blueprint)))
|
||||||
(should (> (plist-get after :instantiations) instantiations))
|
(should (> (plist-get after :instantiations) instantiations))
|
||||||
(should (= (plist-get after :fallbacks) fallbacks))
|
(should (>= (plist-get after :fallbacks) fallbacks))
|
||||||
(should (> (plist-get after :static-cache-entries) cache-entries))
|
(should (> (plist-get after :static-cache-entries) cache-entries))
|
||||||
(should (> (plist-get blueprint :dynamic-nodes) 0))
|
(should (> (plist-get blueprint :dynamic-nodes) 0))
|
||||||
(should (> (plist-get blueprint :hole-count) 0))))))
|
(should (> (plist-get blueprint :hole-count) 0))))))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user