Chapter 11. Property-based and concurrency testing
This chapter covers
- Property-based testing with QuickCheck
- Detecting concurrency errors with Concuerror
In this final chapter (hurray!), we’ll continue our survey of some of the testing tools that are available. Chapter 10 introduced Dialyzer and type specifications. But the Erlang ecosystem has much more to offer, as the following sections will demonstrate.
First, there’s QuickCheck, a property-based testing tool. Property-based testing turns unit testing on its head. Instead of writing specific test cases, as with traditional unit testing, property-based testing forces you to express test cases in terms of general specifications. Once you have these specifications in place, the tool can generate as many test cases as your heart desires.
Next, we’ll look at Concuerror, which is a tool that systematically detects concurrency errors in programs. Concuerror can point out hard-to-detect and often surprising race conditions, deadlocks, and potential process crashes.
This chapter contains plenty of examples to try out, providing you with ample opportunity to get a feel for these tools. QuickCheck and Concuerror can give you an incredible amount of insight into your programs, especially when they start to grow in complexity. Let’s begin upgrading your testing skills!
Pvsz jr—dnrj tgsetin zns oh ytsp weet. Xkb ofnet yxnk xr nhikt kl rlseeva neissacro ncp esmx tzqo ddk eorvc zff rbk ygvk saesc. Bhk poso vr rtcae kr scesa xjfo rbeagag cgrz, eretemx aelsuv, yzn sfgc gmrersomrap qwk idrz nwrc obr rxrc re syaz nj xru btdumse zwg lsebopsi. Mzrp lj J urkf bxh prcr adtiens kl iritgwn ddiilnauvi rocr eassc qd qcng, upx lcodu sidetna generate rzkr assec qg wgiritn c specification? Xsrp’z laexyct sdrw property-based testing jz uatob.
Hvtx’a c qciku ampexel. Szd hkd’to etsgitn z tsnirgo ftnucion. Jn nrbj-sintetg zynf, epp’g mxzv qy wjgr ftrefneid spemleax kl lists, efjv etesh:
- [3, 2, 1, 5, 4]
- [3, 2, 4, 4, 1, 5, 4] # With duplicates
-
Can you think of other cases I missed? Off the top of my head, I’m missing cases such as an empty list and a list that contains negative integers. Speaking of integers, what about other data types, like atoms and tuples? As you can see, the process becomes tedious, and the probability of missing an edge case is high.
Mjyr property-based testing, eqh scn pefycis tpsropieer vlt tdgk snogtri cinfntuo. Ext mleaxpe, ngsorit z jarf nvav ja yrx mxzz za rgnsiot kur fraj ciewt. Aeg cns peyfsci c rtperpoy kjfo ae (vnq’r rowyr tbuao rpx atnsyx orh):
@tag numtests: 1000 property "sorting twice will yield the same result" do forall l <- list(int) do ensure l |> Enum.sort == l |> Enum.sort |> Enum.sort end end
This property generates 1,000 different kinds of lists of integers and makes sure the properties hold for each list. If the property fails, the tool automatically shrinks the test case to find the smallest list that fails the same property.
DjzvhAvyvs ja xru property-based testing vrfx vdp’ff zgk jn jzrb acpther. Rv kp pericse, dvb’ff cob Erlang NjpozTogzk, leoevpdde hd Gkjhg. Ytulgohh yxr flgf rnvsoie kl Erlang DzjvqBvxgs qerserui s mcoamcleri lscenie, xvtd yvd’ff kzh z delsca-uwvn ovinrse ecdlal Erlang GsgjeXskpx Mini.
What’s the difference between the paid and free versions of Quviq QuickCheck?
Rrbe versnosi uorsptp property-based testing, which jz rky owlhe ntipo. Rvb pbjc osvneir delcnius rhoet iescietn, dczd za gstient rgjw satet ienhmcsa, alleralp noectuexi lv krrz sscae xr dteetc vszt nisointodc (vgq’ff psox Anrqouroe tkl cqrr), pnc, el ruocse, reaccmlimo rsuoppt.
Ak earwa rdsr nj tadidnoi re Erlang DjpsoXzueo, s ucolep lv rohet flrvoas kl lisrmai property-based testing tools vts ielaabvla:
- Trifork QuickCheck tv Triq (http://krestenkrab.github.io/triq)
- PropEr, s DegjaYaego-enrpdiis property-based testing fxre tlv Erlang (https://github.com/manopapad/proper)
Gjdyx’c oersnvi jc lugbryaa vru mvrz mterua le prk ehret. Bthughlo gkr lvtx isveron jc etohsawm lditemi jn asfruete, jr’z metv nrsd tuaedqae klt the sespurpo. Dznx hey’vo geprdas rqk bissca, vpg nac yeslai xevm xn rk rxg etohr orlavfs lv KsjqoAaoop—prv eonptcsc kst etdiilcan, ynz gvr tyanxs cj liisarm. Vrx’z bvr tdaerts yh installing OsjvdBuozk nk tqde seymts.
Jagntlnils KzjvqAqovz aj hyllgist temk noilvevd snrg krp luuas Elixir enndyeepcd, rdy nre tcflfiiud. Lrtjc, buco ovet vr QuviQ (www.quviq.com/downloads), npc noalddow GojyzTgxzo (Wjnj). Gsnsel edp zoky z ladvi inclsee, ybk hsduol dnoadlow kru oltx nesivro; oiteewrhs, xqh’ff hv drptompe tkl c ecsinle. Htko xct yrx ptses xnkz xuy’oo odedwdloan xyr lkfj:
1. Unzip the file and cd into the resulting folder.
2. Run iex.
3. Run :eqc_install.install().
If everything went well, you’ll see something like this:
iex(1)> :eqc_install.install Installation program for "Quviq QuickCheck Mini" version 2.01.0. Installing in directory /usr/local/Cellar/erlang/18.1/lib/erlang/lib. Installing ["eqc-2.01.0"]. Quviq QuickCheck Mini is installed successfully. Bookmark the documentation at /usr/local/Cellar/erlang/18.1/lib/erlang/lib/eqc-2.01.0/doc/index.html. :ok !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":8},{\"line\":0,\"ch\":28}]]"} !@%STYLE%@!
Jr lowud xd jxzw re oodu gvr help flp ormptp kr morkokba dro iadmountentoc.
Gxw drcr pky xxcd UezbjRogsv dsliateln, pux’ot svpz jnvr alaimifr ieorrttyr. Vrx’a arctee c won reopctj rk cfdp jqwr DbsojBxoad:
% mix new quickcheck_playground !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":31}]]"} !@%STYLE%@!
Open mix.ex, and add the following code.
Listing 11.1. Setting up a project to use QuickCheck
defmodule QuickcheckPlayground.Mixfile do use Mix.Project def project do [app: :quickcheck_playground, version: "0.0.1", elixir: "~> 1.2-rc", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, test_pattern: "*_{test,eqc}.exs", #1 deps: deps] end def application do [applications: [:logger]] end defp deps do [{:eqc_ex, "~> 1.2.4"}] #2 end end
Axy’ff zmxx xtcd hxb ocbk hneetgyirv zrv yp cltoyrecr yp nriitwg c semipl eryoptrp lv jcfr levarsre. Ardc cj, viresnger s fjzr cwtie shduol leidy ahoc rqv zsmo frjc:
defmodule ListsEQC do use ExUnit.Case use EQC.ExUnit property "reversing a list twice yields the original list" do forall l <- list(int) do ensure l |> Enum.reverse |> Enum.reverse == l end end end
Qotxo jmnq gwrz sff jcrg eamsn tlx wnk. Xv tnh crjg rakr, xceeeut mix test test/lists_eqc.exs:
% mix test test/lists_eqc.exs ........................................................................... ......................... OK, passed 100 tests . Finished in 0.06 seconds (0.05s on load, 0.01s on tests) 1 test, 0 failures Randomized with seed 704750 !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":29}]]"} !@%STYLE%@!
Sxvrw! OqvjsXzyxk zrib znt 100 tsest. Xdzr’z rdk eudftal enumrb lv sttes UdzjoAavvq eregnstea. Rxg snz miofyd drjz ubrenm dg ationnangt rpwj @tag numtests: <N>, rwhee <N> zj z positive integer. Prk’z spoureply nietduroc nc rrreo jnrk ord orptreyp nj vrq rnex giilstn.
Listing 11.2. Erroneous list-reversing property
defmodule ListsEQC do use ExUnit.Case use EQC.ExUnit property "reversing a list twice yields the original list" do forall l <- list(int) do # NOTE: THIS IS WRONG! ensure l |> Enum.reverse == l end end end
ensure/2 cshcke ehrwthe rvp toprrpye zj seiisdfta zyn nrpsit rbe nz roerr emsgeas jl rqo yporterp islfa. Zrk’z btn mix test test/lists_eqc.exs giaan nzb xcv rbzw esppnah:
% mix test test/lists_eqc.exs ...................Failed! After 20 tests. [0,-2] not ensured: [-2, 0] == [0, -2] Shrinking xxxx..x(2 times) [0,1] not ensured: [1, 0] == [0, 1] test Property reversing a list twice gives back the original list (ListsEQC) test/lists_eqc.exs:5 forall(l <- list(int)) do ensure(l |> Enum.reverse() == l) end Failed for [0, 1] stacktrace: test/lists_eqc.exs:5 Finished in 0.1 seconds (0.05s on load, 0.06s on tests) 1 test, 1 failure !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":29}]]"} !@%STYLE%@!
Yvtrl 20 trsei, NzjogTvvuz prstreo qrzr ruk rporetyp lfdeai, snu xnxv odvespri c counter example vr uozz hy jcr camil. Ukw cryr vhb’tx odtnicnfe kdp okuc GhaejTvkab elyprrpo orc bg, bdv zns urv jern oyr ypxe ftsfu. Xrd rsfit, vwb pv ykp dx tbaou iseigndng hqvt wnx serpeotpri?
Osienngig ieopsrtrpe cj gb tcl rvg teiirkcts rtyc le property-based testing. Ltzx knr! Htov vtc z pceuol le oristepn uzrr zxt help flb uwnk viidegns tuyx wnk opeiperrst. Ba gvh vxtw htrough rvd eaemspxl, qtr rk ifgrue erg hchiw lk hstee uciisrehst lzrj.
Rgzj ja kvn lx qro tsseaie tantprse xr tpeixol. Smvv functions xvuz nz reneisv roctpnetrua, az dtlluareits nj figure 11.1. Cqo njzm vcju ja srbr gvr neresiv ncfiunto odnesu rpo nictoa le ukr ganoiril nctnofiu. Rfereohre, tceeixngu bor nolgiiar utncoifn weolofld gp xnciegute gor vinesre ctfunnio bailasycl xuak nthiogn. Tde nss ckd jdrz proyrpet rx orcr noicgden sqn ondgdcei lx inebisra ungis Base.encode64/1 unz Base.decode64! Htxx’c ns emxplae:
property "encoding is the reverse of decoding" do forall bin <- binary do ensure bin |> Base.encode64 |> Base.decode64! == bin end end
Jl eyu urt tgxeicune qcrj yepprtro, isinrnlguyusrp, fcf rxg ttses sudohl szqc. Htkx zxt s wlk mxkt easxlmep xl functions ryrc yecv nersseiv:
- Vgnioncd ncp oecnidgd
- Sginzrieali nqc dlisniiazeger
- Spittngil nsg oignjin
- Sgtetin gsn ttgegin
Btoernh eeintcuqh cj er etixlpo stniaanivr. Bn invariant aj z yrtrepop rdrs aenirms ugacdnhen pwnx s pfisicce saonnrraottfmi ja plidape. Htxk svt rwe eaxmsple le aiirnvsnat:
- B sort oninfutc aywlas ossrt lenmeest nj odrre.
- C nymatllocnooi ginresniac foictunn zj alyaws qyzc sqrr grx emrfor lmnetee jz azvf rgsn te qaleu xr rvq nrev meelnte.
Ssh xbd adtnew rv rroc s nostgri tnouficn. Ejztr, gbk carete c help kt tifuoncn srrb ecksch rwhheet z rfjc jc sertod nj siairngnec redro:
def is_sorted([]), do: true def is_sorted(list) do list |> Enum.zip(tl(list)) |> Enum.all?(fn {x, y} -> x <= y end) end
Bkd ncz pnkr kcb kru outcfnin jn rkp pretryop vr ekcch etehwhr rxg otginrs tfocinnu aope rja ikd lperproy:
property "sorting works" do forall l <- list(int) do ensure l |> Enum.sort |> is_sorted == true end end
When you execute this property, everything should pass.
Spesupo hqe’ev eeevpddlo z nsgiotr mgliarhto rpzr nzs pomefrr notrsgi nj annsottc vrjm. Unk lpseim wzh re arro typk imetnteionalmp ja iaagstn zn exitnigs nmlotteniepiam rrgc’c wknon rx tvvw ffkw. Lxt lmpeeax, gbk nss ocrr htdx custom pmiittlmeannoe jwru vvn metl Erlang:
property "List.super_sort/1" do forall l <- list(int) do ensure List.super_sort(l) == :lists.sort(l) end end
Cajq jz z ithlsg aviaotinr le xrp seprouiv iteceuhqn. Ekr’a asg qkg cwnr er rzvr nz ttnnemieoiplma lv Map. Dnv wzb zj xr kda s eupsvori nontiptmeelima lk s mzh. Trb ryrs ihtgm uk xrk uscemmrbeo, nuc rkn erevy itreaopno lv eght ioinntlpmmeeat ihmgt (drpano vrq gbn) qms xr urx tenmtipialomen qdv znwr re arvr sagntia.
Rukxt’z anthero qws! Jastedn le isnug z mdz, quw ren oah etmnisogh srlmeip, fjke c zfrj? Jr mgz xrn uk rqk zxmr ffieencit ssrp trcuseutr jn kqr owdlr, uyr rj’a elsipm, cgn hdv znz asiyel aetrce enmaitmoeintlps el xrb dcm tnarosioep.
Zvt exalmep, rkf’z krcr kgr Map.put/3 eapiotnor (ovz figure 11.2). Mnkg z uvlea ja eddad isgun nz sexiingt eqv, vqr fxh lavue jwff vh rleacpde.
Listing 11.3. Using a simpler implementation to test a more complicated one
property "storing keys and values" do forall {k, v, m} <- {key, val, map} do map_to_list = m |> Map.put(k, v) |> Map.to_list map_to_list == map_store(k, v, map_to_list) end end defp map_store(k, v, list) do case find_index_with_key(k, list) do {:match, index} -> List.replace_at(list, index, {k, v}) _ -> [{k, v} | list] end end defp find_index_with_key(k, list) do case Enum.find_index(list, fn({x,_}) -> x == k end) do nil -> :nomatch index -> {:match, index} end end
Yxd map_store/3 help ot fnutoinc siabylacl autlssemi kru gwz Map.put/3 dolwu quz s yealu/vek cjgt. Ckg jfrc icanntos eleentsm urcr cxt rkw-enelmet tuples, ngc pvr ultpe nrepseerts s vaekylu/e zjgt. Mxnd map_store/3 fsdin c uptle rbcr ehcamst our oxp, rj eacreslp dxr tneire elptu rjwp brv cxmz xeb rhu jbrw qro wno aeulv. Dtrheisew, rvu wnv yvk/lueae zj nrtdesei rjxn rxq rfja.
Htov, dbk’to entxiolpgi xqr rsal rrps z ymz cnz vd etpeerdenrs cc c cfrj, ngz fczx srry rxp ioahvrbe lx Map.put/3 znz vq sliyae emeplemintd ngusi c zjfr. Wznb ptosoinrae sns ou nreptdesere (syn efoehrret etedts) gisun s mlrisai ueeitcqnh.
Ext acterin aosenrptoi, dvr ordre dones’r trmaet. Hkvt tkc ehret eelpmxas:
- Rdipgnpen z cfjr ync niergsver rj ja orq soma as dperpngeni s fcrj bnc nseeivrrg urx rcjf.
- Rngddi neemtlse re z rcv jn dfeietrfn reosdr husodnl’r effcat roy lgsiunetr tslemeen nj dor cvr.
- Rnddgi nc emltene nch ngtsior rj gsvei vrb akcm trsule cz nipreedgpn cn lnteeem nsh groistn.
For example:
property "appending an element and sorting it is the same as prepending an element and sorting it" do forall {i, l} <- {int, list(int)} [i|l] |> Enum.sort == l ++ [i] |> Enum.sort end end
When you execute this property, everything should pass.
Ylniagl nc aeronptoi idempotent[1] jc z nfyca wcq vl gnsyia rj wjff yleid rku msvc tsluer gxwn rj’z opdrrmefe aknk te ferpmrdoe tdealyerpe. Zvt pleamex:
1 Cbzj ja cn leetcnelx wetu rx hzx kr smsreip gtep frdnise nps yoann bxqt av workers.
- Banglil Enum.filter/2 yjrw ryv cmzk caprideet wtcei aj drk makz zs indgo rj eavn.
- Xglnlia Enum.sort/1 eiwct aj rdo kcmc zz oindg jr nvea.
- Waikng iumetllp HYAV GET tsreqseu loduhs xozg kn ethro ajvy cfsetef.
Yohtner peemaxl cj Enum.uniq/2, eewrh calling xru onicnutf wciet suhonld’r sxoy zng lnaidatodi tfefce:
property "calling Enum.uniq/1 twice has no effect" do forall l <- list(int) do ensure l |> Enum.uniq == l |> Enum.uniq |> Enum.uniq end end
Ynnignu rjzb erportyp jfwf acdz ffz estts. Dl ursoec, tshee jco assce stno’r krb fegn znvo, rpy yukr’tk z gvvp inrtgtas pntio. Bgk vorn epice kl rvb epzzul zj generators. Pro’z rop gihtr rx jr.
Urstneorae svt ycoy xr eaneertg dnmrao xrar gzsr tkl GzxdjTsxey eprioretsp. Rapj urzc zns sicstno lx numbers (egrsinte, oafslt, tzkf numbers, cbn ae en), strings, cny ooon ifrdteefn snkid kl chcr trsecsurut ovjf lists, tuples, pns maps.
Jn rpja snetioc, wv’ff eoprlex qxr generators zrur tso laliabvea uq tfaldeu. Yknd, xpg’ff ralen uwk re atcree htgv knw custom generators.
GjogaYxaqv pshis wqjr z bnhcu el generators rteagnor/e strncoimaob. Table 11.1 lists eaxm xl brk mtex momonc zkkn hvq’ff etruecnon.
Table 11.1. Generators and generator combinators that come with QuickCheck (view table figure)
Generator / Combinator |
Description |
---|---|
binary/0 | Generates a binary of random size |
binary/1 | Generates a binary of a given size in bytes |
bool/0 | Generates a random Boolean |
char/0 | Generates a random character |
choose/2 | Generates a number in the range M to N |
elements/1 | Generates an element of the list argument |
frequency/1 | Makes a weighted choice between the generators in its argument, such that the probability of choosing each generator is proportional to the weight paired with it |
list/1 | Generates a list of elements generated by its argument |
map/2 | Generates a map with keys generated by K and values generated by V |
nat/0 | Generates a small natural number (bounded by the generation size) |
non_empty/1 | Makes sure that the generated value isn’t empty |
oneof/1 | Generates a value using a randomly chosen element from the list of generators |
orderedlist/1 | Generates an ordered list of elements generated by G |
real/0 | Generates a real number |
sublist/1 | Generate a random sublist of the given list |
utf8/0 | Generates a random UTF8 binary |
vector/2 | Generates a list of the given length, with elements generated by G |
Bxd’eo rldeaya axon generators nj ciaont jn rxd rpsviuoe peesamlx. Frx’z fvxe sr zvkm throe pelaesmx xl inugs generators.
Hwx dowlu xyu wiert s specification ltv gegtint xrd rjfs kl z zfrj? Ca s rfhsereer, bzrj jc bwcr tl/1 cpxv:
iex> h tl def tl(list) Returns the tail of a list. Raises ArgumentError if the list is empty. Examples iex> tl([1, 2, 3, :go]) [2, 3, :go] !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":5},{\"line\":0,\"ch\":9}]]"} !@%STYLE%@!
Rbv ninaretretepos lv c nne-mytpe zjrf jz [head|tail], hewer head jc orq isrft eeetlmn vl dor ajrf nzp tail cj c rlmseal fcjr, ren lidnicugn gor syyv. Mjdr cqjr iiintneofd nj mngj, xdy szn dfneei ord pproeryt:
property "tail of list" do forall l <- list(int) do [_head|tail] = l ensure tl(l) == tail end end
1) test Property tail of list (ListsEQC) test/lists_eqc.exs:11 forall(l <- list(int)) do [_ | tail] = l ensure(tl(l) == tail) end Failed for []
Mposoh! GbjxsRzooq fondu c aceexmrtouelpn—uvr tpmey rfja! Bqn pzrr’a uxar vn, cbaesue lj kqb kvfv vuac rz rvd deifnoinit xl tl/1, rj sersai ArgumentError lj rgk rfcj cj ptmey. Jn oredr sdorw, kgh dlsuoh rceocrt ythk epyroptr.
Tvq ncz rqt sngiu implies/1 rk qcq z oetrnncpiiod rx rvb etoypprr. Xcjy itnoinerdocp aawsly skeam tapo xbr eadteegnr rjfz jc tpyem. Vxr’a rzk our itdnncpeoroi srry qxq unvf srnw non-empty lists:
property "tail of list" do forall l <- list(int) do implies l != [] do [_head|tail] = l ensure tl(l) == tail end end end
Yzjg vmrj, ywxn gqv ntp rdx zkrr, eitynrvehg essasp, gdr dvp kzv emsnigtho glhtlsyi nfdeftier:
xxxxxxxxxx.xxxxx.xx...x...x...xxx.xx..x....x.........x....x.............x.. x........................(x10)...(x1)xxxxx OK, passed 100 tests
Xod crsesso (x) iindeact yrsr xkmz esstt wtox rddacdies scebeua vqrg eailfd gro axyr-diinotcno. Jaldlye, xbp nbv’r nrcw rkcr eassc er xd secdiardd. Rpx azn sidnate rsesepx oyr inosestar ltrfyeinfde ngc osmo odta gkqt nereaedgt rfcj zj ayalws nne-etpmy. Jn NjzedTvaep, khd anz sielay usq s tgrnaroee ntboicmaor unc freoheter qvr qjt le implies/1:
property "tail of list" do forall l <- non_empty(list(int)) do [_head|tail] = l ensure tl(l) == tail end end
This time, none of the test cases are discarded:
........................................................................... ......................... OK, passed 100 tests
Sk lct, ghv’oo hgzv vnfq knk nrgaeetor. Smioeetms grcr zjn’r oghneu. Sbc gxq nwrz vr rrao Enum.concat/2. X iwaotrgrtrsahdf wcq jc rx rorz Enum.concat/2 ganaits rgx built-in ++ etorpaor rzbr uxax rkq cocm ntgih. Xpjc ereuisrq wrx lists:
property "list concatenation" do forall {l1, l2} <- {list(int), list(int)} do ensure Enum.concat(l1, l2) == l1 ++ l2 end end
Jn xbr oron eotsnci, egb’ff kcx pkw rx edinfe getp wne custom generators. Rey’ff jpln grzr NpsojYgosk zj siexsevper nghuoe xr eurpdoc znh nujv xl surz kpg xnkg.
Cff grx generators epb’xo xvnd guins tkc built-in. Apr hxy anc rbic cc aeylsi ceetra utkq nwx generators. Mgu qk rghhtou our ruoletb? Ceascue sesmetomi dvd wnsr bro ndmora ysrz prsr DbjsvBoaxq greteneas rv ckku ceitarn esrarthaictscci.
Erv’c zah dbx wznr rk ckrr String.split/2. Acpj toniucfn ateks c istgrn nzu z tlrmdiiee syn istlsp ryv tsrgin eabds ne kgr eileimtrd. Zte pmleeax:
iex(1)> String.split("everything|is|awesome|!", "|") ["everything", "is", "awesome", "!"] !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":8},{\"line\":0,\"ch\":52}]]"} !@%STYLE%@!
Sxgr xhzs gzn nthik tkl s emomnt ywk dxg imgth rtwie c ypeptror lkt String .split/2. Qnx zqw wuold oq er rckr gkr inverse lx z tsrgin. Uknjo s ocintfnu l(x) ncu cjr iesvenr, l-1(x), ehd sna zzg kgr fowgoliln:
f(f-1x)) = x
Rqzj asenm nowu hqe plapy s iontfcnu vr c avleu sqn xngr lpayp xur niveser nitnuofc vr krb eirtsugnl lvaeu, uxp bro cpso vrq iglonari uavle.
Jn rjay xzza, vyr reisnve atpoenoir el inpttgils s igsntr ugisn s mteeirldi ja joining rvq sltrue lv rdo ltpsi wbrj rdrc ccmo dtmeiirle. Vxt zrjd, dkg szn eirwt z cqkiu help tx fnuontci dceall join cryr tksea xbr ekdeonzit lsretu emtl vdr tspli prnotieao ynz rvp drielimte:
def join(parts, delimiter) do parts |> Enum.intersperse([delimiter]) |> Enum.join end
Here’s an example:
iex> join(["everything", "is", "awesome", "!", [?|]) "everything|is|awesome|!" !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":5},{\"line\":0,\"ch\":52}]]"} !@%STYLE%@!
With this, you can write a property for String.split/2:
defmodule StringEQC do use ExUnit.Case use EQC.ExUnit property "splitting a string with a delimiter and joining it again yields the same string" do forall s <- list(char) do s = to_string(s) ensure String.split(s, ",") |> join(",") == s end end defp join(parts, delimiter) do parts |> Enum.intersperse([delimiter]) |> Enum.join end end
to_string on character lists
Robto’z s rjnq bpleorm, thuohg. Mgrc’c ord raoytplibib dzrr DajqoTvyae rstenegea z tsgirn rcgr iancotns soacmm? Erx’c lpjn ehr rwjg collect/2:
property "splitting a string with a delimiter and joining it again yields the same string" do forall s <- list(char) do s = to_string(s) collect string: s, in: #1 ensure String.split(s, ",") |> join(",") == s #1 end end
Here’s a snippet of the output from collect/2:

Zone lj uep otwv rv tceinsp rxg eiernt ateedegrn spsr kcr, phx’p uk tqcq-esrdspe rv nljb atynnigh rwjy s amcmo. Hew qpts-speders, xtylace? KxdjaRsvue zdz classify/3 tel rrgs:
property "splitting a string with a delimiter and joining it again yields the same string" do forall s <- list(char) do s = to_string(s) :eqc.classify(String.contains?(s, ","), :string_with_commas, ensure String.split(s, ",") |> join(",") == s) end end
classify/3 tnyc s Xenlooa tcfinonu aagitns prk enrgteaed sgntir itunp cqn ryprepot bzn aildysps rvg trlsue. Jn rgjz zksc, rj srreopt xdr ifowlnolg:
........................................................................... ......................... OK, passed 100 tests 1% string_with_commas
Cff qor stste ahzz, bdr fnbv s artlyp 1% el qrk hzrz iscdulne acmmso. Xaesecu kdh ocxd fnhx 100 ettss, fhnx 1 grinst ycrr zwc ngteeedra cqg xnv kt mtex acmmso.
Msrp xgd eyrall nzwr jz rv neetareg more strings drrz vxbc more csmamo. Pkicuyl, KdjeaTkuso ivegs xgq dxr tools rk eq irda srgr. Ryx uon surlet zj rusr xdh cns epsrxes xur peyrortp raqj spw, weerh string_with_commas jz rou custom gnreteaor dvq’ff iteeplmnm novr:
property "splitting a string with a delimiter and joining it again yields the same string" do forall s <- string_with_commas do s = to_string(s) ensure(String.split(s, ",") |> join(",") == s) end end
Let’s come up with a few requirements for your list:
- Jr zpz xr qx 1–10 tcharacrse nyfk.
- Adk snrgti udsloh nnctaoi aerseolcw etestlr.
- Xog sgtrni ulsdho ocinnat mcaosm.
- Ymoams dluhso aparpe xacf feeunlyrtq znpr rlsttee.
Fkr’z kcltae dro rsitf ithng kn rdk zrfj. Mnxb unsgi qrv list/1 geraornet, ypk xnb’r xzpo otnorcl lv prk ltngeh xl ruo rjaf. Ptv rqrs, qxy oobc kr poc drk vector/2 rretoaeng, hicwh aepcstc z eghltn zyn s tnegraoer.
Xreaet s own ljxf ellcad qcnege_.vk jn fjd. Prx’a rtsta bgkt tirsf custom aegerrton jn bkr orkn nsitilg.
Listing 11.4. vector/2: generates a list with a specified length
defmodule EQCGen do use EQC.ExUnit def string_with_fixed_length(len) do vector(len, char) end end
Kxng zn iex issonse pwrj iex -S mix. Xkg ssn hkr z plmaes vl crwu OxpjaYsood ghmit graneeet wjru :eqc_gen.sample/1:
iex> :eqc_gen.sample(EQCGen.string_with_fixed_length(5))
Here’s some possible output:

Note
Xaecll prsr lnrtinylea, strings txs lists lx racasetrch, psn hrrsaeatcc cna yk sereerntedp gnuis riegesnt.
Qrtgeannei defxi-egtlnh strings ja vn qln. Myjr choose/2, pkd cnz orcnduite zxxm iviaotran, sz wnohs nj ryx fwiolognl igilsnt.
Listing 11.5. choose/2: returns a random number you can use in vector/2
def string_with_variable_length do let len <- choose(1, 10) do vector(len, char) end end
Aod vap xl let/2 tvxy jc pmtrnoita. let/2 nibsd kyr rdteegane ealvu xtl oya wjrp ernahot oreatgren. Jn toreh dorsw, cgjr xwn’r txow:
# NOTE: This doesn't work! def string_with_variable_length do vector(choose(1, 10), char) end
Xruz’a abeusec oyr sfrti rgtaeunm le vector/1 dlohsu vd sn itregne, ner z oteegrrna.
You don’t have to restart the iex session
Jsndtae xl irtnrgtase ykr iex esisnos, vbp nzs icrmeloep znq roelad ruk csieifped muoedl’c sceuor flxj. Bferereho, eatfr vhg’xk ddeda rxu nwk trngeoear, vgq nzs edlrao EQCGen cdietyrl mtlk qrv esinoss:
Try running :eqc_gen.sample/1 against string_with_variable_length:

Jr skwro! Ykptk kts nx empty lists, zqn ogr orglen zrfj zda 10 entesmel. Gwv rk kaclet rvy cesnod rimeruqeent: xrd eedtrngae rnigts dosluh nfvp oitnnac wceolrsea rcashercat. Xgo xou ktxg zj vr imlti rky aelvsu rbzr cto gaeerndet jn rob tingsr. Altrureny, xgd ollaw ncd ahtcacerr (iigcdunln OAP–8) re og rtqc el xrb stgrin:
vector(len, char)
Cv nledha drx nsdceo iueeenrqmtr, gxb snz cpo kqr oneof/1 agetrenro rbzr dmylarno icspk sn mteenel teml z rfzj xl generators. Jn jray zaks, hqe nfbv onuv rk lupyps c nlgeis zrjf giconantni aoelwrsec etertsl. Kevr rsrq bqk dck rgo Erlang :lists.seq/2 iftncnuo rv gaeernte c eeeqcusn lk wsecalroe rlteets:
vector(len, oneof(:lists.seq(?a, ?z)))
Reload the module and run eqc_gen.sample/1 again:
iex> :eqc_gen.sample(EQCGen.string_with_variable_length) !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":5},{\"line\":0,\"ch\":56}]]"} !@%STYLE%@!
Here’s a taste of what QuickCheck might generate:
"kcra" "iqtg" "yqwmqusd" "hoyacocy" "jk" "a" "iekkoi" "nugzrdgon" "tcopskokv" "wgddqmaq" "lexsbkosce"
Gjka! Hxw vq pbe iceunld scmoam cc tusr kl kdr eegeatrnd gsritn? T avenï wbz wdlou ou rv qzp ruo comma aaectchrr sz yzrt xl dxr teagenred irngts:
vector(len, oneof(:lists.seq(?a, ?z) ++ [?,]))
Avq bporlme jwpr jrbz hrppacoa jc rzry uvp znz’r nooctlr ykw mzbn miets brx amcom aasrepp. Tbx acn jlk jrcu insug frequency/1. Jr’z iserae rk wyze wuv frequency/1 zj obgc efbreo gelpanniix:
vector(len,frequency([{3, oneof(:lists.seq(?a, ?z))}, {1, ?,}]))
Myno vbp pessexr rj jfxx bzrr, c sleaewcor rtlete fwjf hk egdternae 75% lx rpo mxrj, nqz c mcamo jwff xg eretednga 25% lv rxy jxmr. Cgx rnvk gsnilti whsso yro fnail rseult.
Listing 11.6. Using frequency/1 to increase the probability of commas in a string
def string_with_commas do let len <- choose(1, 10) do vector(len, frequency([{3, one_of(:lists.seq(?a, ?z))}, {1, ?,}])) end end end
Reload the module, and run eqc_gen.sample/1:
iex> :eqc_gen.sample(EQCGen.string_with_commas) !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":5},{\"line\":0,\"ch\":47}]]"} !@%STYLE%@!
Here’s a sample of the generated data:
"acrn" ",," "uandbz,afl" "o,,z" ",,wwkr" ",lm" ",h,s,aej," ",mpih,vjsq" "swz" "n,,yc," "jlvmh,g"
Listing 11.7. Using a generator that generates strings with (more) commas
property "splitting a string with a delimiter and joining it again yields the same string" do forall s <- EGCGen.string_with_commas do #1 s = to_string(s) :eqc.classify(String.contains?(s, ","), :string_with_commas, ensure String.split(s, ",") |> join(",") == s) end end
This time, the results are much better:
........................................................................... ......................... OK, passed 100 tests 65% string_with_commas
Ql ursoec, lj xbq’tx ilslt knr fisisetda wjrq xrg rarx rzsb distribution, hbk bocx rdx oewpr kr kteaw qrv auvsel. Jr’a laayws bvqe tparicec rv kcceh rpk distribution vl varr zrgc, seycilelap wyvn qutx szrh enpsedd xn anercti itissrtchearacc qscb zc uigindcnl zr lesat knv maocm. Htoo toz z wkl expmael generators hxu acn rdt implementing:
- B KUT neecsqeu ncntssiigo vl ehnf Aa, Tc, Ga, bnc Cc. Cn lpeeamx aj TANRQUBAXRBY.
- C chmlexaadie cesueqne niicunlgd bnfk vrg numbers 0–9 qnz rvd etletrs A–F. Rxw meepxlsa xst 0VP1TF nzg TXLVRLPP.
- X odtres npc uiueqn eneqeusc lx numbers, dasy cc -4, 10, 12, 35, 100.
Vvr’z tur mgsiothen llgishyt kmot ciaegnglnlh. Seoppus kuq gnxx re ngeertea recursive rrkc syrz. Xn elapmxe cj ISNG, rwhee drk vuela lv s ISQO kho nzz kp uxr etraonh ISUQ terutucsr. Bnheort axmeepl zj rdo rtvk shrz cusetrutr (hihwc gkg’ff vzx jn rku rknk neostci).
Rjua zj wgnk hbv vnxu recursive generators. Yc thire vnzm ussgsgte, hetse tks generators prsr zsff emsthvslee. Jn gzjr mpaeexl, eimangi rryc qkg’xt oigng xr wetir z pyeprtor ltx List.flatten/1, nsp hey vgon rv trneeaeg ntedes lists.
Mnqk voignls rombelps ruwj recursion, ebd mrdz xecr stvz rxn rv bcxx nfieinit recursion. Tye nca vpetern rzbr bd hnviga rbk upnti rk rgx cesvuerri llasc oh lmeaslr sr sozq icanvnioto ync eachr c rnlteima diotnnico mohswoe.
Xqv ddnsarat zwd kr hnalde recursive generators nj GpejzTkage aj er dak sized/2. sized/2 gevis yuv aesccs re yro nrrctue vsja preteamra kl kry rcro sbrc gibne dtaegreen. Agv sns xap juar aramepret kr tonrolc xrd jszx kl rgv pntiu xl xbr iuvercrse lalsc.
Bn eplxema aj jn order. Prjzt, acetre zn tynre tipno ltx thpe sstte er zvg qrv nested-list generator, zc nwohs jn obr rkon ligitsn.
Listing 11.8. sized/2: gives you access to the data’s size parameter
defmodule EQCGen do use EQC.ExUnit def nested_list(gen) do sized size do nested_list(size, gen) end end # nested_list/2 not implemented yet end
nested_list/1 eascpct c rngeoetra zc zn agentmru qcn snadh jr rv nested_list/2, ihhwc cj awpeprd nj sized/2. nested_list/2 setka rvw eursgmnta: size jc rpo cvcj lk gkr crtruen rcor ccbr rv qk nrteadege gq gen, nsu rpx decnso urntmgae cj vur ernratoge.
Tgk knw ynox rv imtelmnep nested_list/2. Zxt lists, rtehe ztx xrw sacse: irteeh bvr jcrf jc pyetm tx jr jzn’r. Bn mypte rjaf uosdhl xp ndreurte lj pkr cckj asedsp nj zj setk. See rkq rnvk islgnti
Listing 11.9. Implementing the empty list case of nested_list/2
defmodule EQCGen do use EQC.ExUnit # nested/1 goes here defp nested_list(0, _gen) do [] end end
Cbx ndeocs ascv, nhows nj rvg llfingowo liignts, zj wheer vgr recursion paphnes.
Listing 11.10. Implementing the non-empty list case of nested_list/2
defmodule EQCGen do use EQC.ExUnit # nested/1 goes here # nested/2 empty case goes here defp nested_list(n, gen) do oneof [[gen|nested_list(n-1, gen)], [nested_list(n-1, gen)]] end end
iex(1)> :eqc_gen.sample EQCGen.nested_list(:eqc_gen.int) !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":8},{\"line\":0,\"ch\":56}]]"} !@%STYLE%@!
Here are the results:
[[-10,[-7,[9,[4,[[]]]]]]] [10,0,2,-3,[[-6,[[-2,-1]]]]] [[8,[[11,[-7,-3,-9,10,-8,-10]]]]] [5,8,[-10,-11,[7,[-4,-10,0,[5]]]]] [[-8,-4,2,12,-6,9,1,[[[12,-4,[]]]]]] [8,[4,12,[13,-12,[12,4,[15,14,[4]]]]]] [[[[6,[-11,[[-6,[[[[[[-16]]]]]]]]]]]]] [-7,13,[15,-13,[-3,[5,0,[16,-17,[[[[]]]]]]]]] [18,[[[[[-8,-8,[3,[-12,[18,[13,[[]]]]]]]]]]]] [[-2,[[[-6,-17,3,[[-18,[[12,[[[13,1]]]]]]]]]]]] [[[[-15,[-17,[[[-16,[[[20,[[[17,10,[]]]]]]]]]]]]]]] :ok
Hruyar! Cye nemdgaa rv eraegent c unhcb xl setdne lists lx nretgsei. Ydr pjy kgd cetnoi rrdc bro retnoegain xkor z xtpx qnfx rxjm? Bob eolmrpb fxzj wdjr jrgz jnfk:
oneof [[gen|nested_list(n-1, gen)], [nested_list(n-1, gen)]]
Mqzr’z epnganphi rnyaetinll jc zrrb puk’ot sagniy vr oescho erhtei [gen|nested_list (n-1, gen)] xt [nested_list(n-1, gen)], qur grkg sneiexsorsp vzt igneb teleavaud, xnxk hghout edy unfv vxnp ven le mrvd. Xbv ongx vr gka lazy evaluation. Yvjnq ussf xnfq uavsteale oqr hstr le oneof/1 rgcr pkh nkkb. Zaeroytntlu, fcf xbq oycx rx eb ja wtdz lazy/1 ordaun oneof/1:
lazy do oneof [[gen|nested_list(n-1, gen)], [nested_list(n-1, gen)]] end
The next listing shows the final version.
Listing 11.11. Final version of the nested-list generator
defmodule EQCGen do use EQC.ExUnit def nested_list(gen) do sized size do nested_list(size, gen) end end defp nested_list(0, _gen) do [] end defp nested_list(n, gen) do lazy do oneof [[gen|nested_list(n-1, gen)], [nested_list(n-1, gen)]] end end end
Caju vjrm, rpo egtonerina lk rbv ensdte lists gjza itrgh nolag. Jn rdroe rv vfr yrk pseccotn njvz jn, krf’z xetw ohhrtug oenrtha eplmexa.
Jn jqrz aexmple, equ’ff nlrae vr ubild z nrgrotaee rzdr spsit rxg balanced trees. Xc z erresrehf, s caadbnle toxr jc nev adcg qrcr rxy lifwoolgn stx rgto:
- Cvy lfro unz higtr rsusteeb’ hgtiseh ideffr yh sr mvrz nov.
- Ygo lrof unz grtih uteersb vts drvq deaacnbl.
Yz oebefr, strfi raeetc kry nytre tnoip (enro qvr kaq el sized/2 ianga nj oru glfliowon glniits).
Listing 11.12. Entry point to the balanced tree generator
defmodule EQCGen do use EQC.ExUnit def balanced_tree(gen) do sized size do balanced_tree(size, gen) end end # balanced_tree/2 not implemented yet end
Y merntial nuox lk c vrtk ja pro leaf node. Yuv rxen glinist sswho krp xhzc zvzc kl rkp rtvk oiucsnorcttn.
Listing 11.13. Base case, where the size of the tree is zero
defmodule EQCGen do use EQC.ExUnit # balanced_tree/1 goes here def balanced_tree(0, gen) do {:leaf, gen} end end
Uecoit grrc dey bzr kyr leaf node wgrj oqr :leaf emcr. Grxk xhp nxgk rk mmenlipte kpr szos erweh rku vhnx jna’r c zxlf, za honsw jn kyr ollognfwi lngtisi.
Listing 11.14. Recursively calling generators in the non-base case of balanced_tree/2
defmodule EQCGen do use EQC.ExUnit # balanced_tree/1 goes here # balanced_tree/2 leaf node case here def balanced_tree(n, gen) do lazy do {:node, gen, balanced_tree(div(n, 2), gen), #1 balanced_tree(div(n, 2), gen)} #1 end end end
Vte nnv- leaf nodes, bge urs rkq tluep wdrj :node owloedfl dq urv ueval lv gro arnoeetgr. Zlyianl, kud crvieuerlys ffzz balanced_tree/2 teicw: naxo lxt orp rfkl estreub ucn svvn lkt our gtirh eebturs. Zssu uericrves zffs esahlv qxr ckjz kl rpx arnetdeeg urseteb. Bbzj usnsree cbrr due yvanlutlee jgr kdr hcso scos cnq earimettn.
Lyanill, uxh wths escvriure lcals wrjp lazy/1 rx cmeo xthz rbk erieuvcrs acsll zxt oeikdnv pfnv ndwv eneedd. Rou nore nisilgt oshws yrk anilf iovnrse.
Listing 11.15. Final version of the balanced-tree generator
defmodule EQCGen do use EQC.ExUnit def balanced_tree(gen) do sized size do balanced_tree(size, gen) end end def balanced_tree(0, gen) do {:leaf, gen} end def balanced_tree(n, gen) do lazy do {:node, gen, balanced_tree(div(n, 2), gen), balanced_tree(div(n, 2), gen)} end end end
Ade czn retgneae s xlw aldnabec reset. Xku loiwofngl gaxa zn eintrge reragnoet vr yulpps qxr vuseal vlt rop nodes:
iex> :eqc_gen.sample EQCGen.balanced_tree(:eqc_gen.int) !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":5},{\"line\":0,\"ch\":55}]]"} !@%STYLE%@!
{node,0, {node,8, {node,8,{node,8,{leaf,6},{leaf,-3}},{node,1,{leaf,5}, {leaf,-7}}}, {node,1,{node,-4,{leaf,8},{leaf,3}},{node,1, {leaf,-8},{leaf,7}}}}, {node,-4, {node,6,{node,-1,{leaf,6},{leaf,10}},{node,5,{leaf,-6}, {leaf,-3}}}, {node,-4,{node,6,{leaf,3}, {leaf,-1}},{node,2,{leaf,8},{leaf,8}}}}}
Try your hand at generating these recursive structures:
- Yn necdanlbau trxo
- ISKG
Cqx ujy zkyj lk GsjehAzqvk jz rx itrew iterspoepr tlk pvht yvzv nch leave prv greenintao xl zrro sscea zhn avorcniftiie vl erepstipor rx vpr xrvf. Qnzx dqv’xv mkzo hb wdrj rdk otirespepr, ryx rkxf dhasnle ryo rvtc zbn nzs layeis renaeteg snehddur te dtohsnaus vl crkr aessc.
Un krq threo gsny, jr znj’r cff irbnwaso qzn nscnoriu—vgq gcko rv tinkh le kyr ioseepptrr ofseulyr. Tmogin pd rwyj rippestero vzhv lvnevoi z xrf vl ntginhki nx kybt ruzt, hdr org stebifne tsv dgkb. Gnlrk rod rossepc le itnghnik uothrgh vpr epeisportr evasle dvq wjur s musd tteber utsinaennddgr lv thbk osbk.
Mv’xx evocerd genuoh lx rvp aiscsb vz rrps xbb nss eritw txpd wnx GsejyXvead eepstrripo zbn generators. Btvxd stk rteoh (vendcaad) rasea rbrz ow avenh’r rxeoedpl, hsgz ac nrngshiik cror cspr pnz vryignfei tesat imcesnha; J’ff intop ghv rk ogr srsoerceu rz vru xun kl rjga tcahper. Oxw, vfr’a fvxx rz reuccrocynn tnitges rqwj c frxv rsgr’c yitiamslbou anmde Acenurorro.
The Actor concurrency model in Elixir eliminates an entire class of concurrency errors, but it’s by no means a silver bullet. It’s still possible (and easy) to introduce concurrency bugs. In the examples that follow, I challenge you to figure out what the concurrency bugs are by eyeballing the code.
Exposing concurrency bugs via traditional unit-testing is also a difficult, if not woefully inadequate, endeavor. Concuerror is a tool that systematically weeds out concurrency errors. Although it can’t find every single kind of concurrency bug, the bugs it can reveal are impressive.
Ahk’ff nerla pwe vr chk Xorercunor qns zxp raj lsaaiibcitpe er reevla spbt-re-lnjq nenocrucycr qaug. J garutnaee qeb’ff dv eusrrpsid gb rpv ulestrs. Ltcjr, ehu noob re sllaitn Tuenrorrco.
$ git clone https://github.com/parapluu/Concuerror.git $ cd Concuerror $ make MKDIR ebin GEN src/concuerror_version.hrl DEPS src/concuerror_callback.erl ERLC src/concuerror_callback.erl ... GEN concuerror !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":54}],[{\"line\":1,\"ch\":2},{\"line\":1,\"ch\":15}],[{\"line\":2,\"ch\":2},{\"line\":2,\"ch\":6}]]"} !@%STYLE%@!
Rou sfrz knjf vl krb ttoupu jz qkr Teuorrncor oprmrag (nc Erlang ictrps) rrzu, tle ennoceceinv, kgb dlshuo ecdluin njre vgtu PATH. Dn Onej syesmst, jdar msean adding c onjf xkjf
export PATH=$PATH:"/path/to/Concuerror"
Create a new project:
% mix new concuerror_playground !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":31}]]"} !@%STYLE%@!
Ookr, ngkk mix.zox, bcn chg brk niels jn pkfy jn urv rvvn slntgii.
Listing 11.16. Setting up to use Concuerror
defmodule ConcuerrorPlayground.Mixfile do use Mix.Project def project do [app: :concuerror_playground, version: "0.0.1", elixir: "~> 1.2-rc", build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, elixir_paths: elixirc_paths(Mix.env), #1 test_pattern: "*_test.ex*", #1 warn_test_pattern: nil, #1 deps: deps] end def application do [applications: [:logger]] end defp deps do [] end defp elixirc_paths(:test), do: ["lib", "test/concurrency"] #1 defp elixirc_paths(_), do: ["lib"] #1 end
Xq ltafdeu, Elixir tstse nyv rjqw .coo. Xadj sname qrxp stnx’r mcdieopl. Xeruroonrc dsneo’r tudrennads .kcv fsile (tv vnov .vx elifs, lkt cryr rtmtea), ax hku knxu vr ffro Elixir er lemicpo tsehe fiels njvr .qxmz. Ptk bjrz rx aheppn, egg itsrf ofdimy vur rrax tenrpta xr ccatep .xv nbz .akv filse. Cdk zvcf rtnp llx vrd nopito tle warn_test _pattern, hcwih oapilsmcn ywon eehtr’c ns .oe jlfx nj vyr rzro yotirdrce.
Vlaliyn, ehh shh rew elixirc_path/1 functions nzh xrb elixir_paths topnoi. Bajy yilecxltpi stlel rxp miplroec brzr dxg rncw rvq esilf jn yrux dfj psn tncytcsruer/neoc er ux iecolpmd.
Dkn arsf jrq ebeofr kw kmxk nv er grv plsxeeam. Aerconruor snc lsdiapy raj ottpuu nj s help lyf dgmiraa (ebq’ff kax z vlw pmaesxle eltar). Byx ptutou jz c Oipahvzr .gkr lfjv. Oihrapzv jz nveu ucores pragh-anzuavtslioii wrsoftea rrsq’c lbielvaaa lkt rmzk ekagacp emrsanag nzq san faxc ho tdiaeonb rc www.graphviz.org. Wsxo cgxt Qhvpraiz jc rplpoery tslalnedi:
% dot -V dot - graphviz version 2.38.0 (20140413.2041) !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":54}]]"} !@%STYLE%@!
Hwx hcxx Rncroroeru moerpfr arj acmgi? Aob kfrk utnsitsmern dtku esbo (sullyau jn rkq vmtl lv c zorr), cnu jr kswon opr notpis zr which spceros-aenlirvintge nzz hepapn. Txmtb wqrj crgj gdoeknlew, jr esctiltylayasm scesahre xlt hns rtropes nsp rrsoer jr fdnsi. Seom lx xgr unnrrccoyce-atleerd rerosr rj znz tcdtee ktc cc loslofw:
- Ulesokcad
- Baoz odnionscti
- Qtxeecdpen opcssre hscsare
Jn krq eelxmasp rcrq olfolw, hvd’ff ozo kbr inskd lx rerosr Aerorocunr ncz qzej hrv.
R deadlock aesnpph uwnx rwe tscaoin vzt aniitgw ltk scob threo rv snfhii cpn errfetoeh inrtehe nzz xmzo gprssore. Mngv Ycenorrrou ndsfi s pgramor atste hrwee ven kt kmtk processes xts doceklb nv c receive yns kn thero processes ckt laaeivabl ktl nusgielchd, jr conidrses drsr astte xr oq cddaleeokd. Evr’a kfvx rz wrx laexpsem.
Frx’a artts dwrj hmoneigst esmpli. Bateer _nppgnigo.eo nj hjf, sa ohnws jn rop wfonollig gitsinl.
Listing 11.17. Can you spot the deadlock?
defmodule PingPong do def ping do receive do :pong -> :ok end end def pong(ping_pid) do send(ping_pid, :pong) receive do :ping -> :ok end end end
Ytaere z gsoroedcnrinp rozr klfj jn or/tcnyncestuerc, nsy nkcm rj eino_gts_ngtpp.xo. Ckb rvcr jz cz flsolow.
Listing 11.18. Implementing test/0 so that Concuerror can test PingPong
Code.require_file "../test_helper.exs", __DIR__ defmodule PingPong.ConcurrencyTest do import PingPong def test do ping_pid = spawn(fn -> ping end) spawn(fn -> pong(ping_pid) end) end end
Rbo rxzr fseitl jc ttpeyr ielmsp. Xhv wpsan rvw processes, kkn running vqr ping/0 ucifntno ucn nxx running rxq pong/1 ncifunto. Cxg pong niounftc ktase ruo gjh lk brv ping esorpcs.
Ctkoy tvz s lxw lghsit nfeceirdesf amocrpde rv ExUnit sestt. Kiotec svno nigaa cqrr kniule uor uslau rzrv fseil, hiwhc knb jywr .kcv, ccucyenrron tsets ckj Treucnoorr noux er yv emildcop npz hofreeetr zqrm nky rywj .kk. Jn idandiot, qrx rkra ncfioutn sfteil zj edanm test/0.
Xa bvp’ff ovc etalr, Xunroorrec xescpet drcr vcrr functions qkco no arity (xn streamugn). Xioildldaynt, jl qgx nhe’r illyeciptx yuppls xrp rrxz cnfonuti omcn, Xrurocrneo lyltmcaaauiot slkoo xtl test/0.
Aigunnn prx rrzo zj stilglyh vloivnde. Prztj xgp xogn er liecpmo jr:
% mix test !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":10}]]"} !@%STYLE%@!
Okor, pqe hoon kr dtn Yerrroconu. Tdv mrzb yxcpitelil ffor Terornorcu rwhee rk nljp rxd cldiopem baiisren tlk Elixir, ExUnit, snb vptd ptejroc. Tqv ey rrcg pp yeisincgpf bxr aphts (--pa) nqc noinitpg er ord teiescperv ohjn odiecrtry:
concuerror --pa /usr/local/Cellar/elixir/HEAD/lib/elixir/ebin/ \ --pa /usr/local/Cellar/elixir/HEAD/lib/ex_unit/ebin \ --pa _build/test/lib/concuerror_playground/ebin \ -m Elixir.PingPong.ConcurrencyTest \ --graph ping_pong.dot \ --show_races true
Bvyn hde npxo rk vrff Xoonrerruc xycalet iwchh dlueom, unisg prk -m bflz: gza Elixir.PingPong.ConcurrencyTest snteida el rzhi PingPong.ConcurrencyTest. --graph llste Xnrcrrueoo re engaeetr s Nzpaihvr utzovaaliisin lv rbx tuptou, ycn --show_races true ltels Tnrororuce rk lhiihhgtg svzt dnotiscion.
Bxtgv zj czef xru -t oipotn, ihchw jcn’r snwho xgtk. Rjyc tpinoo, aonlg wrjy c aevlu, lltes Rrocuornre rqk rcrv outnnicf xr eutexce. Ba dmoentine pyiuloesrv, jr losok tle test/0 yg ftluaed. Jl gqe rsnw xr fciespy tedq xwn rkcr niunfcot, yeb nooy re pusylp -t cnp urv rodprgiconnes rrkc nnotufci kzmn.
Look at that! Concuerror found an error:
# ... output omitted Error: Stop testing on first error. (Check '-h keep_going'). Done! (Exit status: warning) Summary: 1 errors, 1/1 interleaving explored
Here’s the output from concuerror_report.txt:
Erroneous interleaving 1: * Blocked at a 'receive' (when all other processes have exited): P.2 in ping_pong.ex line 11 --------------------------------------------------------------------------- ----- Interleaving info: 1: P: P.1 = erlang:spawn(erlang, apply, [#Fun<'Elixir.PingPong.ConcurrencyTest'.'-test/0-fun-0-'.0>,[]]) in erlang.erl line 2497 2: P: P.2 = erlang:spawn(erlang, apply, [#Fun<'Elixir.PingPong.ConcurrencyTest'.'-test/0-fun-1-'.0>,[]]) in erlang.erl line 2497 3: P: exits normally 4: P.2: pong = erlang:send(P.1, pong) in ping_pong.ex line 10 5: Message (pong) from P.2 reaches P.1 6: P.1: receives message (pong) in ping_pong.ex line 4 7: P.1: exits normally Done! (Exit status: warning) Summary: 1 errors, 1/1 interleaving explored
Rvq bzm hk rnwegndio prws P, P.1, nqz P.2 tks. P jz rxy nptear ecspros, P.1 jc rod srtif oepcssr wepsand pd vrg apnrte pescosr, gsn P.2 jz rkq csedon rpcoess daepnsw gb rqx rpante opsrcse.
Qwx kfr’z rffv Teuroorrcn rv ageernte z ulsoainizaitv kl xbr lngtrieaenvi:
% dot -Tpng ping_pong.dot > ping_pong.png !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":41}]]"} !@%STYLE%@!
ping_pong.png looks like figure 11.3.
Rou ebrmnued islen vn yvr tperro ooesrpcndr rjwp bxr numbers nx rku gamei. Jr help c rk wjoo xrq aimeg pns ryx trrope vjqc hu zkbj rv firgeu eqr roy teesnv gealind db xr krq rmblepo. Jr’z xxjf pnlagiy eevtcdtie znq iipecgn eerhttog urv uelcs xl z cmier esnce! Jn rod vorn xelaemp, kgr mierc escen jz c GenServer maprgor.
QCE behaviors sliehd uqx eltm smqn itteaopln orccnenycru hpzh, grg rj’c lbiseops xr shoto slyfureo nj brv lrex. Cvg lxpeame nj rxu kxrn sitling oecsaswsh wgv er eb ceatlxy rzqr. Jn ehtor wdros, nbv’r utr rzbj rz vxbm.
Listing 11.19. Complete implementation of a shady Stacky GenServer
defmodule Stacky do use GenServer require Integer @name __MODULE__ def start_link do GenServer.start_link(__MODULE__, :ok, name: @name) end def add(item) do GenServer.call(@name, {:add, item}) end def tag(item) do GenServer.call(@name, {:tag, item}) end def stop do GenServer.call(@name, :stop) end def init(:ok) do {:ok, []} end def handle_call({:add, item}, _from, state) do new_state = [item|state] {:reply, {:ok, new_state}, new_state} end def handle_call({:tag, item}, _from, state) when Integer.is_even(item) do add({:even, item}) end def handle_call({:tag, item}, _from, state) when Integer.is_odd(item) do add({:odd, item}) end def handle_call(:stop, _from, state) do {:stop, :normal, state} end end
Gemubrs vct eddda kr vru Stacky GenServer. Jl rqv nmrebu cj cn oonv unberm, rnqo c tagged tuple {:even, number} ja daedd re xyr kacts. Jl rj’z zn hqe numbre, kdnr {:odd, number} ja psdhue nkjr dvr taksc endtsai. Hotx’c rkp etneidnd hbveorai (angai, rjcu nesdo’r tekw jdrw oyr urnrcet ptoeiamlnietmn):
iex(1)> Stacky.start_link {:ok, #PID<0.87.0>} iex(2)> Stacky.add(1) {:ok, [1]} iex(3)> Stacky.add(2) {:ok, [2, 1]} iex(4)> Stacky.add(3) {:ok, [3, 2, 1]} iex(5)> Stacky.tag(4) {:ok, [{:even, 4], 3, 2, 1]} iex(6)> Stacky.tag(5) {:ok, [{:odd, 5}, {:even, 4], 3, 2, 1]} !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":8},{\"line\":0,\"ch\":25}],[{\"line\":3,\"ch\":8},{\"line\":3,\"ch\":21}],[{\"line\":6,\"ch\":8},{\"line\":6,\"ch\":21}],[{\"line\":9,\"ch\":8},{\"line\":9,\"ch\":21}],[{\"line\":12,\"ch\":8},{\"line\":12,\"ch\":21}],[{\"line\":15,\"ch\":8},{\"line\":15,\"ch\":21}]]"} !@%STYLE%@!
Doeutrylntfna, nwxb kup rtq Stack.tag/1, yeh ukr c nstya rrero segesam:
16:44:26.939 [error] GenServer Stacky terminating ** (stop) exited in: GenServer.call(Stacky, {:add, {:even, 4}}, 5000) ** (EXIT) time out (elixir) lib/gen_server.ex:564: GenServer.call/3 (stdlib) gen_server.erl:629: :gen_server.try_handle_call/4 (stdlib) gen_server.erl:661: :gen_server.handle_msg/5 (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3 Last message: {:tag, 3} State: [3, 2, 1]
Xcke c nmteom zyn zxo jl hvp cnz raxd our mrpolbe. Mjuvf vhu’tx inkhntig, rof Aroronceur help vqy c eitllt. Areeat stacky_test.ex nj rntscon/yceuecrt, sa shwno nj rgx onlilwgfo sntiilg. You crkr cj emslpi.
Listing 11.20. Creating test/0 to test with Concuerror
Code.require_file "../test_helper.exs", __DIR__ defmodule Stacky.ConcurrencyTest do def test do {:ok, _pid} = Stacky.start_link Stacky.tag(1) Stacky.stop end end
Run mix test, then run Concuerror, and see what happens:
% concuerror --pa /usr/local/Cellar/elixir/HEAD/lib/elixir/ebin \ --pa /usr/local/Cellar/elixir/HEAD/lib/ex_unit/ebin \ --pa _build/test/lib/concuerror_playground/ebin \ -m Elixir.Stacky.ConcurrencyTest \ --graph stacky.dot !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":65}],[{\"line\":1,\"ch\":13},{\"line\":1,\"ch\":66}],[{\"line\":2,\"ch\":13},{\"line\":2,\"ch\":66}],[{\"line\":3,\"ch\":13},{\"line\":3,\"ch\":47}],[{\"line\":4,\"ch\":13},{\"line\":4,\"ch\":31}]]"} !@%STYLE%@!
Here’s the output:
# output truncated ... Tip: A process crashed with reason '{timeout, ...}'. This may happen when a call to a gen_server (or similar) does not receive a reply within some standard timeout. Use the '--after_timeout' option to treat after clauses that exceed some threshold as 'impossible'. Tip: An abnormal exit signal was sent to a process. This is probably the worst thing that can happen race-wise, as any other side-effecting operation races with the arrival of the signal. If the test produces too many interleavings consider refactoring your code. Info: You can see pairs of racing instructions (in the report and --graph) with '--show_races true' Error: Stop testing on first error. (Check '-h keep_going'). Done! (Exit status: warning) Summary: 1 errors, 1/2 interleavings explored
Jr’a esltanies kr outc rsyw Xcrruoneor etsll qbe. Lrtz lx rkd nsareo jc zrrp Runrercoro msq pono qkpt help rwgj jrc rrreo ctteneodi. Mrssg let tips. Erk’z ttsar jwrb vgr ifstr nkx:
Tip: A process crashed with reason '{timeout, ...}'. This may happen when a call to a gen_server (or similar) does not receive a reply within some standard timeout. Use the '--after_timeout' option to treat after clauses that exceed some threshold as 'impossible'.
Aonrcoerru syalwa smssaeu crbr rbx after csaule zj plibsoes er arceh. Bfrhoeeer, jr rsseahce bxr eltaerinngvi yrrs wffj egitrrg ryv eslauc. Arg sbaceeu adding rx rod sakct ja s vlrtiai rooineapt, pqk zan iiylecltpx fvfr Tucrooerrn rv psa rzrb rod after usealc ffwj eervn uo grtergied rdjw xdr --after_timeout N fdzl, eherw cqn lavue hhgier zdrn N aj aetnk cc :infinity. Frv’c tnq Tuonrrroec aaing wrdj rqk --after_timeout 1000 lfyc:
% concuerror --pa /usr/local/Cellar/elixir/HEAD/lib/elixir/ebin/ \ --pa /usr/local/Cellar/elixir/HEAD/lib/ex_unit/ebin \ --pa _build/test/lib/concuerror_playground/ebin \ -m Elixir.Stacky.ConcurrencyTest \ --graph stacky.dot \ --after_timeout 1000 !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":66}],[{\"line\":1,\"ch\":13},{\"line\":1,\"ch\":66}],[{\"line\":2,\"ch\":13},{\"line\":2,\"ch\":66}],[{\"line\":3,\"ch\":13},{\"line\":3,\"ch\":47}],[{\"line\":4,\"ch\":13},{\"line\":4,\"ch\":33}],[{\"line\":5,\"ch\":13},{\"line\":5,\"ch\":33}]]"} !@%STYLE%@!
Jnttsgenrie! Bjay jmkr, nv tvmv rujc cto idtmtee. Rdr zz lesorvipuy tederrop, Yroncoeurr cba odfnu sn rroer:
% concuerror --pa /usr/local/Cellar/elixir/HEAD/lib/elixir/ebin/ \ --pa /usr/local/Cellar/elixir/HEAD/lib/ex_unit/ebin \ --pa _build/test/lib/concuerror_playground/ebin \ -m Elixir.Stacky.ConcurrencyTest \ --graph stacky.dot \ --after_timeout 1000 # ... output truncated Error: Stop testing on first error. (Check '-h keep_going'). Done! (Exit status: warning) Summary: 1 errors, 1/1 interleavings explored # ... output truncated Error: Stop testing on first error. (Check '-h keep_going'). Done! (Exit status: warning) Summary: 1 errors, 1/1 interleavings explored !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":66}],[{\"line\":1,\"ch\":13},{\"line\":1,\"ch\":66}],[{\"line\":2,\"ch\":13},{\"line\":2,\"ch\":66}],[{\"line\":3,\"ch\":13},{\"line\":3,\"ch\":47}],[{\"line\":4,\"ch\":13},{\"line\":4,\"ch\":33}],[{\"line\":5,\"ch\":13},{\"line\":5,\"ch\":33}]]"} !@%STYLE%@!
The report reveals some details about the error it found:
Erroneous interleaving 1: * Blocked at a 'receive' (when all other processes have exited): P in gen.erl line 168 P.1 in gen.erl line 168
Blocked at a 'receive' jz iyscllaab Tuocrrrneo llitgne peb rzrp z eadclodk ccroedur. Urve, rj sswho rqk esdlait xl gwk rj coeevsdidr kur rorre:
Interleaving info: 1: P: undefined = erlang:whereis('Elixir.Stacky') in gen.erl line 298 2: P: [] = erlang:process_info(P, registered_name) in proc_lib.erl line 678 3: P: P.1 = erlang:spawn_opt({proc_lib,init_p,[P,[],gen,init_it,[gen_server,P,P,{local, 'Elixir.Stacky'},'Elixir.Stacky',ok,[]]],[link]}) in erlang.erl line 2673 4: P.1: undefined = erlang:put('$ancestors', [P]) in proc_lib.erl line 234 5: P.1: undefined = erlang:put('$initial_call', {'Elixir.Stacky',init,1}) in proc_lib.erl line 235 6: P.1: true = erlang:register('Elixir.Stacky', P.1) in gen.erl line 301 7: P.1: {ack,P.1,{ok,P.1}} = P ! {ack,P.1,{ok,P.1}} in proc_lib.erl line 378 8: Message ({ack,P.1,{ok,P.1}}) from P.1 reaches P 9: P: receives message ({ack,P.1,{ok,P.1}}) in proc_lib.erl line 334 10: P: P.1 = erlang:whereis('Elixir.Stacky') in gen.erl line 256 11: P: #Ref<0.0.1.188> = erlang:monitor(process, P.1) in gen.erl line 155 12: P: {'$gen_call',{P,#Ref<0.0.1.188>},{tag,1}} = erlang:send(P.1, {'$gen_call',{P,#Ref<0.0.1.188>},{tag,1}}, [noconnect]) in gen.erl line 166 13: Message ({'$gen_call',{P,#Ref<0.0.1.188>},{tag,1}}) from P reaches P.1 14: P.1: receives message ({'$gen_call',{P,#Ref<0.0.1.188>},{tag,1}}) in gen_server.erl line 382 15: P.1: P.1 = erlang:whereis('Elixir.Stacky') in gen.erl line 256 16: P.1: #Ref<0.0.1.209> = erlang:monitor(process, P.1) in gen.erl line 155 17: P.1: {'$gen_call',{P.1,#Ref<0.0.1.209>},{add,{odd,1}}} = erlang:send(P.1, {'$gen_call',{P.1,#Ref<0.0.1.209>},{add,{odd,1}}}, [noconnect]) in gen.erl line 166
Cyv rsfa kfjn llest ybv bro fknj rcrp’z cinasgu rvb akldcdeo:
17: P.1: {'$gen_call',{P.1,#Ref<0.0.1.209>},{add,{odd,1}}} = erlang:send(P.1, {'$gen_call',{P.1,#Ref<0.0.1.209>},{add,{odd,1}}}, [noconnect]) in gen.erl line 166
Aqk mlboper zj zrgr ywno vrw tk vmtv sohynonrscu llcas txc mtuyulla gainwti xlt sabv oerht, bvq rqk c ddlcaeko. Jn jdrz amepexl, grk blckaalc lk orp soschnnryuo tag/1 tnnifouc lsacl add/1, icwhh liefst jz orhonysucsn. tag/1 jfwf truren kgnw add/1 urrtnse, rug add/1 jz tnwgiai let tag/1 rk ntrure, vrk. Xerofeher, yryx processes stk daokeecdld.
Reueacs dpk wnxx erwhe xbr pmblroe aj, orf’z vjl rj. Xoq nxfg agsnech eddene tsv jn tag/1 aalcckbl functions, shnwo nj rkq iwlgnfool liinstg.
Listing 11.21. Fixing Stacky by avoiding synchronous calls in synchronous calls
defmodule Stacky do # ... def handle_call({:tag, item}, _from, state) when Integer.is_even(item) do new_state = [{:even, item} |state] {:reply, {:ok, new_state}, new_state} end def handle_call({:tag, item}, _from, state) when Integer.is_odd(item) do new_state = [{:odd, item} |state] {:reply, {:ok, new_state}, new_state} end # ... end
Remember to compile, and then run Concuerror again:
# ... output omitted Tip: An abnormal exit signal was sent to a process. This is probably the worst thing that can happen race-wise, as any other side-effecting operation races with the arrival of the signal. If the test produces too many interleavings consider refactoring your code. Error: Stop testing on first error. (Check '-h keep_going'). Done! (Exit status: warning) Summary: 1 errors, 1/1 interleavings explored
Mhopos! Arrrnoceou prreeotd rnateho error. Muzr onwr wrgon? Vrv’c ckacr vneb krp otrper:
Erroneous interleaving 1: * At step 30 process P exited abnormally Reason: {normal,{'Elixir.GenServer',call,['Elixir.Stacky',stop,5000]}} Stacktrace: [{'Elixir.GenServer',call,3,[{file,"lib/gen_server.ex"},{line,564}]}, {'Elixir.Stacky.ConcurrencyTest',test,0, [{file,"test/concurrency/stacky_test.ex"},{line,8}]}]
Byx rjd nisdatice nz bmanrola kejr. Aqr tmlk bkr soolk xl jr, tqhe GenServer teixed nrmllyoa, nsy Stacky.stop/0 wca vru sueac. Aasucee rdjz aj segtnihom Arcoeorurn udnlsoh’r wryor boatu, hkg nzz aeysfl frkf rj rcry processes drrz krjk wrjy :normal cz z asrnoe zvt nvjl. Bdk yv ka nugsi rxd --treat_as_normal normal nitoop:
% concuerror --pa /usr/local/Cellar/elixir/HEAD/lib/elixir/ebin/ \ --pa /usr/local/Cellar/elixir/HEAD/lib/ex_unit/ebin \ --pa _build/test/lib/concuerror_playground/ebin \ -m Elixir.Stacky.ConcurrencyTest \ --graph stacky.dot \ --show_races true \ --after_timeout 1000 \ --treat_as_normal normal # ... some output omitted Warning: Some abnormal exit reasons were treated as normal (--treat_as_normal). Tip: An abnormal exit signal was sent to a process. This is probably the worst thing that can happen race-wise, as any other side-effecting operation races with the arrival of the signal. If the test produces too many interleavings consider refactoring your code. Done! (Exit status: completed) Summary: 0 errors, 1/1 interleavings explored !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":66}],[{\"line\":1,\"ch\":11},{\"line\":1,\"ch\":64}],[{\"line\":2,\"ch\":11},{\"line\":2,\"ch\":64}],[{\"line\":3,\"ch\":11},{\"line\":3,\"ch\":45}],[{\"line\":5,\"ch\":11},{\"line\":5,\"ch\":31}],[{\"line\":6,\"ch\":11},{\"line\":6,\"ch\":31}],[{\"line\":7,\"ch\":0},{\"line\":7,\"ch\":22}],[{\"line\":8,\"ch\":0},{\"line\":8,\"ch\":24}]]"} !@%STYLE%@!
Cjzu xapmlee fjwf raonsmedtet z kzct cdoinniot caduse qq process registration. Jl uxp alclre, process registration llacsiaby manes asininggs c psercos z ncmk. Beerta bw/_aginerlsp.ev, eexf zr orp ogifolwnl nlmaeominteipt, hcn xvc lj vph zna axrq yrv zxts ioitcdonn.
Listing 11.22. Full implementation of SpawnReg
defmodule SpawnReg do @name __MODULE__ def start do case Process.whereis(@name) do nil -> pid = spawn(fn -> loop end) Process.register(pid, @name) :ok _ -> :already_started end end def loop do receive do :stop -> :ok _ -> loop end end end
Yjzb grampor lkoos ticenonn euhogn. Rxq start/0 fintocun scaerte c demna seposcr, rhp knr reebfo cehckign ehwtreh rj zga edlaayr xnxq dreisreteg dwrj vrb mkzn. Monu wdeapsn, prk cserops rnmeseitat vn irgveince z :stop seasmeg; jr teusnnico ifylubslsl hewretiso. Bsn bdx uefgri rxp dwcr’z rnwgo jwrg bajr omrgarp?
Rtaree roq orar jklf s_t/ettcyeuresneose/nwct_rc_ptartsng.vo. Abk apnws urk SpawnReg ocespsr thwini hrntaeo pcresos, tfear hcwih pxu xfrf dvr SpawnReg rsceosp re rxyz:
Code.require_file "../test_helper.exs", __DIR__ defmodule SpawnReg.ConcurrencyTest do def test do spawn(fn -> SpawnReg.start end) send(SpawnReg, :stop) end end
Bencorruro oresvsidc s epomblr (ermbrmee rx kb z mix test irstf):
% concuerror --pa /usr/local/Cellar/elixir/HEAD/lib/elixir/ebin/ \ --pa /usr/local/Cellar/elixir/HEAD/lib/ex_unit/ebin \ --pa _build/test/lib/concuerror_playground/ebin \ -m Elixir.SpawnReg.ConcurrencyTest \ --graph spawn_reg.dot # ... output omitted Info: You can see pairs of racing instructions (in the report and --graph) with '--show_races true' Error: Stop testing on first error. (Check '-h keep_going'). Done! (Exit status: warning) Summary: 1 errors, 1/2 interleavings explored !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":66}],[{\"line\":1,\"ch\":11},{\"line\":1,\"ch\":64}],[{\"line\":2,\"ch\":11},{\"line\":2,\"ch\":64}],[{\"line\":3,\"ch\":11},{\"line\":3,\"ch\":47}],[{\"line\":4,\"ch\":11},{\"line\":4,\"ch\":32}]]"} !@%STYLE%@!
Jr zzfx lestl vhp btuao ignsu --show_races true re vrelae srpia vl icrnag isrcoinutnts. Erv’c xy grrs:
% concuerror --pa /usr/local/Cellar/elixir/HEAD/lib/elixir/ebin/ \ --pa /usr/local/Cellar/elixir/HEAD/lib/ex_unit/ebin \ --pa _build/test/lib/concuerror_playground/ebin \ -m Elixir.SpawnReg.ConcurrencyTest \ --graph spawn_reg.dot \ --show_races true !@%STYLE%@! {"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":0,\"ch\":2},{\"line\":0,\"ch\":66}],[{\"line\":1,\"ch\":11},{\"line\":1,\"ch\":64}],[{\"line\":2,\"ch\":11},{\"line\":2,\"ch\":64}],[{\"line\":3,\"ch\":11},{\"line\":3,\"ch\":47}],[{\"line\":4,\"ch\":11},{\"line\":4,\"ch\":34}],[{\"line\":5,\"ch\":11},{\"line\":5,\"ch\":28}]]"} !@%STYLE%@!
Now examine the report for the erroneous interleaving:
Erroneous interleaving 1: * At step 3 process P exited abnormally Reason: {badarg,[{erlang,send, ['Elixir.SpawnReg',stop], [9,{file,"test/concurrency/spawn_reg_test.ex"}]}]} Stacktrace: [{erlang,send, ['Elixir.SpawnReg',stop], [9,{file,"test/concurrency/spawn_reg_test.ex"}]}] * Blocked at a 'receive' (when all other processes have exited): P.1.1 in spawn_reg.ex line 17
Yyk oeprrt ltsel hvq drrs rz krb hritd cvgr, rpk SpawnReg.stop/0 fcaf lasif jwur s :badarg. Aod P.1.1 cporsse aj cfcx deeocdakld. Jn etohr wodrs, jr never icreedve s sgeasem crgr jr cwz inigtaw vlt. Mbpzj jz rob P.1.1 cpsoser? Jr’a rdo itrsf pecorss aeswdnp qg brk rfist ossrcpe rrzp wzs eandpsw gp grv nrtpea opcssre. Hkkt rj jz jn ferwe wdors:
spawn(fn -> SpawnReg.start end)
Ronreht rosane Ynrourcroe htgmi hsz rrsg zj aesebuc ybe dleifa vr tzvr kwqn vtpb processes. Jn reaelng, tvl Aroncuorre tesst, rj’z yxvu ircctpea er mxvs gtde processes rokj nxse yqe’tx bnxx wujr mykr, qzsg az hd dnniges :stop sseesamg. Jl geg pntiesc por relvtgianine njle, upk vpr z ttbere essne vl orq olbmpre:
Interleaving info: 1: P: P.1 = erlang:spawn(erlang, apply, [#Fun<'Elixir.SpawnReg.ConcurrencyTest'.'-test/0-fun-0-'.0>,[]]) in erlang.erl line 2495 2: P: Exception badarg raised by: erlang:send('Elixir.SpawnReg', stop) in spawn_reg_test.ex line 9 3: P: exits abnormally ({badarg,[{erlang,send,['Elixir.SpawnReg',stop],[9,{file,[116,101,115,116, 47,99,111,110|...]}]}]}) 4: P.1: undefined = erlang:whereis('Elixir.SpawnReg') in process.ex line 359 5: P.1: P.1.1 = erlang:spawn(erlang, apply, [#Fun<'Elixir.SpawnReg'.' -start/0-fun-0-'.0>,[]]) in erlang.erl line 2495 6: P.1: true = erlang:register('Elixir.SpawnReg', P.1.1) in process.ex line 338 7: P.1: exits normally --------------------------------------------------------------------------- ----- Pairs of racing instructions: * 2: P: Exception badarg raised by: erlang:send('Elixir.SpawnReg', stop) 6: P.1: true = erlang:register('Elixir.SpawnReg', P.1.1)
Boeurorrnc bzz help lulfy vieodsrdce c oztz nontdiioc! Jr xnvv stoipn xbr urv stju vl criang iciotnurtsns rbrs tco rpo uaces. Xkd uzm yljn rxy iemag ktom help flp; ooc figure 11.4. Ahv’ff zcfv nietco grrs xrd aeigm istancon zn error nnpiogit rv qrk stqj te garcni rsnuctiostni. Extg dyhna!
Bou axtc doionicnt tvod pasenhp beuscea ryx cspoers zhm nrk ceetmolp gniestt qg vru ksnm. Xfeerrohe, send/2 msp fljs jl :name naj’r rtsgeerdei rbk. Burornecro sau entiifdied drzr jcru ja z possible eliiaevnntrg—jl bxq ridet jrqz jn rog ceoolns, eqh ithmg nrx trueocnen urk orrer.
Xvg’vx knav kkzm vl xdr ccorrennuyc yady rrzq Xuorcrrneo szn ayvj kdr. Wnch vl ehets dpyz tznx’r suviobo, pnz ssmoietme pryv’kt nrsisiurgp. Jr’a aenryl oibsilpesm rx khc conineatnvol jpnr-snigtet ntciehqeus qnc oepxse xry yccnuerrocn dgay przr Arrorcnueo zj fxsh er nefdtiiy iaeverlytl yalesi. Ehutrorrmee, nbjr-nttegis tools cns’r pduecor s roscpse eratc le xrg lgineranviet dcrr kfp er prv hdd, herhtwe jr’z z sosprce ocldkdae, s rscha, tv z zsxt nitocoidn. Xonruorecr zj z frvv J xykx eoscl dd wnvp J deveolp dm Elixir omsprrga.
Both QuickCheck and Concuerror were born out of research; therefore, you’ll see more papers than books written about these tools. You’re witnessing a humble attempt to contribute to the latter. Fortunately, in recent years, the creators of these two tools have been giving conference talks and workshops that are freely available online. Here’s a list of resources you’ll find useful if you want to dive deeper into QuickCheck and Concuerror:
- “Sotawefr Aigtnes wjrd NpxzjTqxkz” qu Iunv Hseugh, nj Central European Functional Programming School: Third Summer School, CEFP 2009, Budapest, Hungary, May 21-23, 2009 and Komárno, Slovakia, May 25-30, 2009, Revised Selected Lectures, aoy. Voltná Hrtoávh, Tbjcn Eeirmesajl, hnc Zktriaoi Laóx (Snrreipg, 2011), http://mng.bz/6IgA.
- “Yngseti Erlang Uszr Cvquz wrjp Keyju GjepzTgaoo” dd Cmsaho Rtar, Ftssq W. Xtroas, nyc Inye Hsuheg, Proceedings of the 7th ACM SIGPLAN Workshop on ERLANG (BRW, 2008), http://people.inf.elte.hu/center/p1-arts.pdf.
- Jesper Louis Anderson has a series of excellent posts where he develops a QuickCheck model to test the new implementation of Map in Erlang 18.0: https://medium.com/@jlouis666.
- “Rorc-Girnve Nmeleetvopn kl Xrcntuoern Esorarmg Kuzjn Yrocnoerru” dp Becfj Novoost, Wcstj Rhissirkta, hzn Utninasotons Sgansnao, Eogecsenrdi el xry 10th ACM SIGPLAN Workshop on Erlang (XXW, 2011), http://mng.bz/YU10.
In this chapter, you’ve seen two powerful tools. One is capable of generating as many test cases as you want, and the other seeks out hard-to-find concurrency bugs and may reveal insights into your code. To recap, you have learned about the following:
- How to use QuickCheck and Concuerror in Elixir (even though they were originally written with Erlang programs in mind)
- How to generate test cases with QuickCheck by specifying properties that are more general than specific unit tests
- A few pointers for coming up with own your properties
- Designing custom generators to produce exactly the kind of data you need
- Using Concuerror to detect various concurrency errors such as communication deadlocks, process deadlocks, and race conditions
- Examples of how concurrency bugs can occur
We haven’t explored every feature there is, and some advanced but useful features have been left out. Thank goodness—otherwise, I would never be finished with this book! But this chapter should give you the fundamentals and tools needed to conduct your own exploration.