Когда вы видите ошибку 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
в спецификации.