(.module: [library [lux (#- and for) ["." meta] ["." debug] [abstract ["." monad (#+ do)]] [control [pipe (#+ case>)] ["." maybe] ["." try] ["." exception (#+ exception:)] ["." io] [concurrency ["." atom (#+ Atom)] ["." async (#+ Async) ("#\." monad)]] ["<>" parser ["<.>" code]]] [data ["." product] ["." name] ["." text ["%" format (#+ format)]] [collection ["." list ("#\." functor mix)] ["." set (#+ Set)] ["." dictionary #_ ["#" ordered (#+ Dictionary)]]]] [time ["." instant] ["." duration (#+ Duration)]] [math ["." random (#+ Random) ("#\." monad)] [number (#+ hex) ["n" nat] ["f" frac]]] [macro [syntax (#+ syntax:)] ["." code]] [world ["." program]]]]) (type: .public Tally (Record {#successes Nat #failures Nat #expected_coverage (Set Name) #actual_coverage (Set Name)})) (def: (total parameter subject) (-> Tally Tally Tally) {#successes (n.+ (value@ #successes parameter) (value@ #successes subject)) #failures (n.+ (value@ #failures parameter) (value@ #failures subject)) #expected_coverage (set.union (value@ #expected_coverage parameter) (value@ #expected_coverage subject)) #actual_coverage (set.union (value@ #actual_coverage parameter) (value@ #actual_coverage subject))}) (def: start Tally {#successes 0 #failures 0 #expected_coverage (set.empty name.hash) #actual_coverage (set.empty name.hash)}) (template [ ] [(def: Tally (revised@ .++ ..start))] [success_tally #successes] [failure_tally #failures] ) (type: .public Assertion (Async [Tally Text])) (type: .public Test (Random Assertion)) (def: separator text.new_line) (def: .public (and' left right) (-> Assertion Assertion Assertion) (let [[read! write!] (: [(Async [Tally Text]) (async.Resolver [Tally Text])] (async.async [])) _ (|> left (async.upon! (function (_ [l_tally l_documentation]) (async.upon! (function (_ [r_tally r_documentation]) (write! [(..total l_tally r_tally) (format l_documentation ..separator r_documentation)])) right))) io.run!)] read!)) (def: .public (and left right) (-> Test Test Test) (do {! random.monad} [left left] (\ ! each (..and' left) right))) (def: context_prefix text.tab) (def: .public (context description) (-> Text Test Test) (random\each (async\each (function (_ [tally documentation]) [tally (|> documentation (text.all_split_by ..separator) (list\each (|>> (format context_prefix))) (text.interposed ..separator) (format description ..separator))])))) (def: failure_prefix "[Failure] ") (def: success_prefix "[Success] ") (def: .public failure (-> Text Test) (|>> (format ..failure_prefix) [..failure_tally] async\in random\in)) (def: .public (assertion message condition) (-> Text Bit Assertion) (<| async\in (if condition [..success_tally (format ..success_prefix message)] [..failure_tally (format ..failure_prefix message)]))) (def: .public (test message condition) (-> Text Bit Test) (random\in (..assertion message condition))) (def: .public (lifted message random) (-> Text (Random Bit) Test) (random\each (..assertion message) random)) (def: pcg_32_magic_inc Nat (hex "FEDCBA9876543210")) (type: .public Seed Nat) (def: .public (seed value test) (-> Seed Test Test) (function (_ prng) (let [[_ result] (random.result (random.pcg_32 [..pcg_32_magic_inc value]) test)] [prng result]))) (def: failed? (-> Tally Bit) (|>> (value@ #failures) (n.> 0))) (def: (times_failure seed documentation) (-> Seed Text Text) (format documentation ..separator ..separator "Failed with this seed: " (%.nat seed))) (exception: .public must_try_test_at_least_once) (def: .public (times amount test) (-> Nat Test Test) (case amount 0 (..failure (exception.error ..must_try_test_at_least_once [])) _ (do random.monad [seed random.nat] (function (recur prng) (let [[prng' instance] (random.result (random.pcg_32 [..pcg_32_magic_inc seed]) test)] [prng' (do {! async.monad} [[tally documentation] instance] (if (..failed? tally) (in [tally (times_failure seed documentation)]) (case amount 1 instance _ (|> test (times (-- amount)) (random.result prng') product.right))))]))))) (def: (description duration tally) (-> Duration Tally Text) (let [successes (value@ #successes tally) failures (value@ #failures tally) missing (set.difference (value@ #actual_coverage tally) (value@ #expected_coverage tally)) unexpected (set.difference (value@ #expected_coverage tally) (value@ #actual_coverage tally)) report (: (-> (Set Name) Text) (|>> set.list (list.sorted (\ name.order <)) (exception.listing %.name))) expected_definitions_to_cover (set.size (value@ #expected_coverage tally)) unexpected_definitions_covered (set.size unexpected) actual_definitions_covered (n.- unexpected_definitions_covered (set.size (value@ #actual_coverage tally))) coverage (case expected_definitions_to_cover 0 "N/A" expected (let [missing_ratio (f./ (n.frac expected) (n.frac (set.size missing))) max_percent +100.0 done_percent (|> +1.0 (f.- missing_ratio) (f.* max_percent))] (if (f.= max_percent done_percent) "100%" (let [raw (|> done_percent %.frac (text.replaced/1 "+" ""))] (|> raw (text.clip 0 (if (f.< +10.0 done_percent) 4 ... X.XX 5 ... XX.XX )) (maybe.else raw) (text.suffix "%"))))))] (exception.report ["Duration" (%.duration duration)] ["# Tests" (%.nat (n.+ successes failures))] ["# Successes" (%.nat successes)] ["# Failures" (%.nat failures)] ["# Expected definitions to cover" (%.nat expected_definitions_to_cover)] ["# Actual definitions covered" (%.nat actual_definitions_covered)] ["# Pending definitions to cover" (%.nat (n.- actual_definitions_covered expected_definitions_to_cover))] ["# Unexpected definitions covered" (%.nat unexpected_definitions_covered)] ["Coverage" coverage] ["Pending definitions to cover" (report missing)] ["Unexpected definitions covered" (report unexpected)]))) (def: failure_exit_code +1) (def: success_exit_code +0) (def: .public (run! test) (-> Test (Async Nothing)) (do async.monad [pre (async.future instant.now) .let [seed (instant.millis pre) prng (random.pcg_32 [..pcg_32_magic_inc seed])] [tally documentation] (|> test (random.result prng) product.right) post (async.future instant.now) .let [duration (instant.span pre post) _ (debug.log! (format documentation text.new_line text.new_line (..description duration tally) text.new_line))]] (async.future (\ program.default exit (case (value@ #failures tally) 0 ..success_exit_code _ ..failure_exit_code))))) (def: (|cover'| coverage condition) (-> (List Name) Bit Assertion) (let [message (|> coverage (list\each %.name) (text.interposed " & ")) coverage (set.of_list name.hash coverage)] (|> (..assertion message condition) (async\each (function (_ [tally documentation]) [(revised@ #actual_coverage (set.union coverage) tally) documentation]))))) (def: (|cover| coverage condition) (-> (List Name) Bit Test) (|> (..|cover'| coverage condition) random\in)) (def: (|for| coverage test) (-> (List Name) Test Test) (let [context (|> coverage (list\each %.name) (text.interposed " & ")) coverage (set.of_list name.hash coverage)] (random\each (async\each (function (_ [tally documentation]) [(revised@ #actual_coverage (set.union coverage) tally) documentation])) (..context context test)))) (def: (name_code name) (-> Name Code) (code.tuple (list (code.text (name.module name)) (code.text (name.short name))))) (syntax: (reference [name .identifier]) (do meta.monad [_ (meta.export name)] (in (list (name_code name))))) (def: coverage_separator Text (text.of_char 31)) (def: encoded_coverage (-> (List Text) Text) (list\mix (function (_ short aggregate) (case aggregate "" short _ (format aggregate ..coverage_separator short))) "")) (def: (coverage module encoding) (-> Text Text (Set Name)) (loop [remaining encoding output (set.of_list name.hash (list))] (case (text.split_by ..coverage_separator remaining) (#.Some [head tail]) (recur tail (set.has [module head] output)) #.None (set.has [module remaining] output)))) (template [ ] [(syntax: .public ( [coverage (.tuple (<>.many .any)) condition .any]) (let [coverage (list\each (function (_ definition) (` ((~! ..reference) (~ definition)))) coverage)] (in (list (` ((~! ) (: (.List .Name) (.list (~+ coverage))) (~ condition)))))))] [cover' ..|cover'|] [cover ..|cover|] ) (syntax: .public (for [coverage (.tuple (<>.many .any)) test .any]) (let [coverage (list\each (function (_ definition) (` ((~! ..reference) (~ definition)))) coverage)] (in (list (` ((~! ..|for|) (: (.List .Name) (.list (~+ coverage))) (~ test))))))) (def: (covering' module coverage test) (-> Text Text Test Test) (let [coverage (..coverage module coverage)] (|> (..context module test) (random\each (async\each (function (_ [tally documentation]) [(revised@ #expected_coverage (set.union coverage) tally) documentation])))))) (syntax: .public (covering [module .identifier test .any]) (do meta.monad [.let [module (name.module module)] definitions (meta.definitions module) .let [coverage (|> definitions (list\mix (function (_ [short [exported? _]] aggregate) (if exported? (#.Item short aggregate) aggregate)) #.End) ..encoded_coverage)]] (in (list (` ((~! ..covering') (~ (code.text module)) (~ (code.text coverage)) (~ test))))))) (exception: .public (error_during_execution {error Text}) (exception.report ["Error" (%.text error)])) (def: .public (in_parallel tests) (-> (List Test) Test) (case (list.size tests) 0 (random\in (async\in [..start ""])) expected_tests (do random.monad [seed random.nat .let [prng (random.pcg_32 [..pcg_32_magic_inc seed]) run! (: (-> Test Assertion) (|>> (random.result prng) product.right (function (_ _)) "lux try" (case> (#try.Success output) output (#try.Failure error) (..assertion (exception.error ..error_during_execution [error]) false)) io.io async.future async\conjoint)) state (: (Atom (Dictionary Nat [Tally Text])) (atom.atom (dictionary.empty n.order))) [read! write!] (: [Assertion (async.Resolver [Tally Text])] (async.async [])) _ (io.run! (monad.each io.monad (function (_ [index test]) (async.upon! (function (_ assertion) (do io.monad [[_ results] (atom.update! (dictionary.has index assertion) state)] (if (n.= expected_tests (dictionary.size results)) (let [assertions (|> results dictionary.entries (list\each product.right))] (write! [(|> assertions (list\each product.left) (list\mix ..total ..start)) (|> assertions (list\each product.right) (text.interposed ..separator))])) (in [])))) (run! test))) (list.enumeration tests)))]] (in read!))))