Ответ 1
(Это новый подход, eval
- и binding
-бесплатный. Как обсуждалось в
комментарии к этому ответу, использование eval
проблематично, потому что
он предотвращает закрытие тестов в лексических средах, которые они кажутся
(поэтому (let [x 1] (deftest easy (is (= x 1))))
no
более длинные работы). Я оставляю исходный подход в нижней половине
ответ, ниже горизонтального правила.)
Подход macrolet
Реализация
Протестировано с помощью Clojure 1.3.0-beta2; вероятно, он должен работать с 1.2.x как хорошо.
(ns deftest-magic.core
(:use [clojure.tools.macro :only [macrolet]]))
(defmacro with-test-tags [tags & body]
(let [deftest-decl
(list 'deftest ['name '& 'body]
(list 'let ['n `(vary-meta ~'name update-in [:tags]
(fnil into #{}) ~tags)
'form `(list* '~'clojure.test/deftest ~'n ~'body)]
'form))
with-test-tags-decl
(list 'with-test-tags ['tags '& 'body]
`(list* '~'deftest-magic.core/with-test-tags
(into ~tags ~'tags) ~'body))]
`(macrolet [~deftest-decl
~with-test-tags-decl]
[email protected])))
Использование
... лучше всего продемонстрировать с помощью набора (прохождения) тестов:
(ns deftest-magic.test.core
(:use [deftest-magic.core :only [with-test-tags]])
(:use clojure.test))
;; defines a test with no tags attached:
(deftest plain-deftest
(is (= :foo :foo)))
(with-test-tags #{:foo}
;; this test will be tagged #{:foo}:
(deftest foo
(is true))
(with-test-tags #{:bar}
;; this test will be tagged #{:foo :bar}:
(deftest foo-bar
(is true))))
;; confirming the claims made in the comments above:
(deftest test-tags
(let [plaintest-tags (:tags (meta #'plain-deftest))]
(is (or (nil? plaintest-tags) (empty? plaintest-tags))))
(is (= #{:foo} (:tags (meta #'foo))))
(is (= #{:foo :bar} (:tags (meta #'foo-bar)))))
;; tests can be closures:
(let [x 1]
(deftest lexical-bindings-no-tags
(is (= x 1))))
;; this works inside with-test-args as well:
(with-test-tags #{:foo}
(let [x 1]
(deftest easy (is true))
(deftest lexical-bindings-with-tags
(is (= #{:foo} (:tags (meta #'easy))))
(is (= x 1)))))
Замечания по дизайну:
-
Мы хотим, чтобы проект
macrolet
, описанный в вопрос текст произведение. Мы заботимся о возможности гнездаwith-test-tags
и сохранение возможности определения тестов чьи тела близки к лексическим средам, которые они определены в. -
Мы будем
macrolet
tingdeftest
развернуть доclojure.test/deftest
с соответствующими метаданными, прикрепленными к имя теста. Важная часть здесь состоит в том, чтоwith-test-tags
вставляет соответствующий набор тегов прямо в определение пользовательский локальныйdeftest
внутри формыmacrolet
; однажды компилятор приближается к расширению формdeftest
, набор тегов будут связаны с кодом. -
Если мы оставим это, тесты, определенные внутри вложенных
with-test-tags
будет только помечен тегами, переданными в самой внутренней формыwith-test-tags
. Таким образом, мы имеемwith-test-tags
такжеmacrolet
символwith-test-tags
сам по себе очень похож локальныйdeftest
: он расширяется до вызова верхнего уровняwith-test-tags
с соответствующими тегами, введенными в множества ярлыков. -
Предполагается, что внутренняя форма
with-test-tags
в(with-test-tags #{:foo} (with-test-tags #{:bar} ...))
развернуть до
(deftest-magic.core/with-test-tags #{:foo :bar} ...)
(если действительноdeftest-magic.core
- пространство именwith-test-tags
определяется в). Эта форма немедленно расширяется в привычнуюmacrolet
, с символамиdeftest
иwith-test-tags
локально привязан к макросам с правильными наборами тегов, их.
(Оригинальный ответ обновлен некоторыми заметками о дизайне, некоторые перефразировать и переформатировать и т.д. Код не изменяется.)
Подход binding
+ eval
.
(См. также https://gist.github.com/1185513 для версии
дополнительно используя macrolet
, чтобы избежать пользовательского верхнего уровня
deftest
.)
Реализация
Ниже приведено тестирование на работу с Clojure 1.3.0-beta2; с
^:dynamic
удалена часть, она должна работать с 1.2:
(ns deftest-magic.core)
(def ^:dynamic *tags* #{})
(defmacro with-test-tags [tags & body]
`(binding [*tags* (into *tags* ~tags)]
[email protected]))
(defmacro deftest [name & body]
`(let [n# (vary-meta '~name update-in [:tags] (fnil into #{}) *tags*)
form# (list* 'clojure.test/deftest n# '~body)]
(eval form#)))
Использование
(ns example.core
(:use [clojure.test :exclude [deftest]])
(:use [deftest-magic.core :only [with-test-tags deftest]]))
;; defines a test with an empty set of tags:
(deftest no-tags
(is true))
(with-test-tags #{:foo}
;; this test will be tagged #{:foo}:
(deftest foo
(is true))
(with-test-tags #{:bar}
;; this test will be tagged #{:foo :bar}:
(deftest foo-bar
(is true))))
Замечания по дизайну
Я думаю, что в этом случае разумное использование eval
приводит к
полезное решение. Базовая конструкция (основанная на "binding
-able Var"
идея) состоит из трех компонентов:
-
Динамически связываемый Var -
Макрос*tags*
- который связан при компиляции время для набора меток, которые будут использоваться формамиdeftest
для украшения испытаний. Мы добавляем теги по умолчанию, поэтому его начальный значение#{}
. -
A
with-test-tags
, который устанавливает подходящий для*tags*
. -
Пользовательский макрос
deftest
, который расширяется до формыlet
, напоминающей это (следующее разложение, слегка упрощенное для ясность):(let [n (vary-meta '<NAME> update-in [:tags] (fnil into #{}) *tags*) form (list* 'clojure.test/deftest n '<BODY>)] (eval form))
<NAME>
и<BODY>
являются аргументами,deftest
, вставлен в соответствующие точки, соответствующие части шаблона расширения с синтаксисом.
Таким образом, расширение пользовательского deftest
является формой let
, в которой,
во-первых, название нового теста готовится путем украшения данного
символ с метаданными :tags
; то форма a clojure.test/deftest
используя это украшенное имя; и, наконец, последняя форма
передается eval
.
Ключевым моментом здесь является то, что выражения (eval form)
здесь
оценивается всякий раз, когда пространство имен, содержащееся в них, скомпилировано AOT или
требуется в первый раз за всю жизнь JVM, запускающей этот
код. Это точно так же, как (println "asdf")
в
верхний уровень (def asdf (println "asdf"))
, который напечатает asdf
всякий раз, когда пространство имен AOT-компилируется или требуется для первого
время; на самом деле, верхний уровень (println "asdf")
действует аналогично.
Это объясняется тем, что компиляция в Clojure - это просто
оценка всех форм высшего уровня. В (binding [...] (deftest ...)
,
binding
- форма верхнего уровня, но она возвращается только тогда, когда deftest
и наш пользовательский deftest
расширяется до формы, которая возвращается, когда
eval
делает. (С другой стороны, способ require
выполняет верхний уровень
код в уже скомпилированных пространствах имен, так что если в вашем коде (def t
(System/currentTimeMillis))
значение t
будет
зависят от того, когда вам требуется пространство имен, а не когда оно было
скомпилированный, как можно определить, экспериментируя с AOT-скомпилированным кодом
- это просто способ работы Clojure. Используйте read-eval, если хотите, чтобы
константы, встроенные в код.)
По сути, пользовательский deftest
запускает компилятор (через eval
) в
время выполнения во время макрообмена. Fun.
Наконец, когда форма a deftest
помещается внутри формы with-test-tags
form
of (eval form)
будет подготовлен с привязками
установленный на with-test-tags
на месте. Таким образом, определение теста
будут украшены соответствующим набором тегов.
На REPL
user=> (use 'deftest-magic.core '[clojure.test :exclude [deftest]])
nil
user=> (with-test-tags #{:foo}
(deftest foo (is true))
(with-test-tags #{:bar}
(deftest foo-bar (is true))))
#'user/foo-bar
user=> (meta #'foo)
{:ns #<Namespace user>,
:name foo,
:file "NO_SOURCE_PATH",
:line 2,
:test #<user$fn__90 [email protected]>,
:tags #{:foo}} ; <= note the tags
user=> (meta #'foo-bar)
{:ns #<Namespace user>,
:name foo-bar,
:file "NO_SOURCE_PATH",
:line 2,
:test #<user$fn__94 [email protected]>,
:tags #{:foo :bar}} ; <= likewise
user=> (deftest quux (is true))
#'user/quux
user=> (meta #'quux)
{:ns #<Namespace user>,
:name quux,
:file "NO_SOURCE_PATH",
:line 5,
:test #<user$fn__106 [email protected]>,
:tags #{}} ; <= no tags works too
И только для того, чтобы быть уверенным, что рабочие тесты определены...
user=> (run-tests 'user)
Testing user
Ran 3 tests containing 3 assertions.
0 failures, 0 errors.
{:type :summary, :pass 3, :test 3, :error 0, :fail 0}