У меня есть сложные спецификации для моих данных - как генерировать образцы? - PullRequest
6 голосов
/ 08 марта 2019

Моя спецификация Clojure выглядит следующим образом:

(spec/def ::global-id string?)
(spec/def ::part-of string?)
(spec/def ::type string?)
(spec/def ::value string?)
(spec/def ::name string?)
(spec/def ::text string?)
(spec/def ::date (spec/nilable (spec/and string? #(re-matches #"^\d{4}-\d{2}-\d{2}$" %))))
(spec/def ::interaction-name string?)
(spec/def ::center (spec/coll-of string? :kind vector? :count 2))
(spec/def ::context- (spec/keys :req [::global-id ::type]
                                :opt [::part-of ::center]))
(spec/def ::contexts (spec/coll-of ::context-))
(spec/def ::datasource string?)
(spec/def ::datasource- (spec/nilable (spec/keys :req [::global-id ::name])))
(spec/def ::datasources (spec/coll-of ::datasource-))
(spec/def ::location string?)
(spec/def ::location-meaning- (spec/keys :req [::global-id ::location ::contexts ::type]))
(spec/def ::location-meanings (spec/coll-of ::location-meaning-))
(spec/def ::context string?)
(spec/def ::context-association-type string?)
(spec/def ::context-association-name string?)
(spec/def ::priority string?)
(spec/def ::has-context- (spec/keys :req [::context ::context-association-type ::context-association-name ::priority]))
(spec/def ::has-contexts (spec/coll-of ::has-context-))
(spec/def ::fact- (spec/keys :req [::global-id ::type ::name ::value]))
(spec/def ::facts (spec/coll-of ::fact-))
(spec/def ::attribute- (spec/keys :req [::name ::type ::value]))
(spec/def ::attributes (spec/coll-of ::attribute-))
(spec/def ::fulltext (spec/keys :req [::global-id ::text]))
(spec/def ::feature- (spec/keys :req [::global-id ::date ::location-meanings ::has-contexts ::facts ::attributes ::interaction-name]
                                :opt [::fulltext]))
(spec/def ::features (spec/coll-of ::feature-))
(spec/def ::attribute- (spec/keys :req [::name ::type ::value]))
(spec/def ::attributes (spec/coll-of ::attribute-))
(spec/def ::ioi-slice string?)
(spec/def ::ioi- (spec/keys :req [::global-id ::type ::datasource ::features ::attributes ::ioi-slice]))
(spec/def ::iois (spec/coll-of ::ioi-))
(spec/def ::data (spec/keys :req [::contexts ::datasources ::iois]))
(spec/def ::data- ::data)

Но он не может генерировать образцы с:

(spec/fdef data->graph
  :args (spec/cat :data ::xml-spec/data-))

(println (stest/check `data->graph))

, тогда он не будет генерировать с исключением: Couldn't satisfy such-that predicate after 100 tries.

Очень удобно генерировать спецификацию автоматически с помощью stest/check, но как помимо спецификации также есть генераторы?

1 Ответ

8 голосов
/ 08 марта 2019

Когда вы видите ошибку Couldn't satisfy such-that predicate after 100 tries. при генерации данных из спецификаций, общей причиной является спецификация s/and, потому что спецификация строит генераторы для s/and спецификаций, основанных исключительно на первой внутренней спецификации.

Эта спецификация, скорее всего, могла вызвать это, потому что первая внутренняя спецификация / предикат в s/and равна string?, а следующий предикат является регулярным выражением:

(s/def ::date (s/nilable (s/and string? #(re-matches #"^\d{4}-\d{2}-\d{2}$" %))))

Если вы попробуете генератор string?, вы увидите, что его генерация вряд ли когда-либо будет соответствовать вашему регулярному выражению:

(gen/sample (s/gen string?))
=> ("" "" "X" "" "" "hT9" "7x97" "S" "9" "1Z")

test.check попытается (100 раз по умолчанию) получить значение, удовлетворяющее условиям such-that, затем выдаст исключение, которое вы видите, если это не так.

Генерация дат

Вы можете реализовать собственный генератор для этой спецификации несколькими способами. Вот генератор test.check, который будет создавать локальные строки даты ISO:

(def gen-local-date-str
  (let [day-range (.range (ChronoField/EPOCH_DAY))
        day-min (.getMinimum day-range)
        day-max (.getMaximum day-range)]
    (gen/fmap #(str (LocalDate/ofEpochDay %))
              (gen/large-integer* {:min day-min :max day-max}))))

Этот подход получает диапазон действительных дней эпохи, использует его для управления диапазоном генератора large-integer*, затем fmap s LocalDate/ofEpochDay над сгенерированными целыми числами.

(def gen-local-date-str
  (gen/fmap #(-> (Instant/ofEpochMilli %)
                 (LocalDateTime/ofInstant ZoneOffset/UTC)
                 (.toLocalDate)
                 (str))
            gen/large-integer))

Это начинается с генератора по умолчанию large-integer и использует fmap для предоставления функции, которая создает java.time.Instant из сгенерированного целого числа, преобразует его в java.time.LocalDate и преобразует его в строку, которая удобно соответствует вашему формату строки даты. (Это немного проще на Java 9 и выше с java.time.LocalDate/ofInstant.)

Другой подход может использовать генератор строк на основе регулярных выражений test.chuck или другие классы / форматеры даты. Обратите внимание, что оба моих примера будут генерировать годы, которые являются эонами до / после -9999 / + 9999, что не будет соответствовать вашему регулярному выражению \d{4} года, но генератор должен выдавать удовлетворительные значения достаточно часто, чтобы это не имело значения для вашего использования. дело. Существует много способов создания значений даты!

(gen/sample gen-local-date-str)
=>
("1969-12-31"
 "1970-01-01"
 "1970-01-01"
 ...)

Использование пользовательских генераторов со спецификациями

Затем вы можете связать этот генератор с вашей спецификацией, используя s/with-gen:

(s/def ::date
  (s/nilable
   (s/with-gen
    (s/and string? #(re-matches #"^\d{4}-\d{2}-\d{2}$" %))
    (constantly gen-local-date-str))))

(gen/sample (s/gen ::date))
=>
("1969-12-31"
 nil ;; note that it also makes nils b/c it's wrapped in s/nilable
 "1970-01-01"
 ...)

Вы также можете предоставить «автономные» пользовательские генераторы для определенных функций спецификации, которые принимают переопределения карты, если вы не хотите связывать пользовательский генератор напрямую с определением спецификации:

(gen/sample (s/gen ::data {::date (constantly gen-local-date-str)}))

Используя эту спецификацию и генератор, я смог сгенерировать вашу большую спецификацию ::data, хотя выходные данные были очень большими из-за некоторых спецификаций сбора. Вы также можете контролировать их размер во время генерации, используя опции :gen-max в спецификации.

...