PRMICOEYQ5HHHN6PP5SBSBYU6ZZWLCD3DSUPDYCS2DVJ4XLNRCDAC ExUnit.start()
defmodule CustomCustomsTest douse ExUnit.Casedoctest CustomCustomstest "greets the world" doassert CustomCustoms.hello() == :worldendtest "grouper" doassert CustomCustoms.groups("a") == ["a"]assert CustomCustoms.groups("ab\nc") == ["ab", "c"]assert CustomCustoms.groups("a\nb\nc") == ["a", "b", "c"]endtest "unioner" doassert CustomCustoms.unioner(["a"]) == "a"assert CustomCustoms.unioner(["ab", "a"]) == "a"endtest "count group" doassert CustomCustoms.count_group("a") == 1assert CustomCustoms.count_group("ab") == 2assert CustomCustoms.count_group("a\nb\nc") == 0assert CustomCustoms.count_group("ab\nac") == 1endtest "part 1" doFile.read!("input")|> String.split("\n\n", trim: true)|> Enum.map(&CustomCustoms.count_group/1)|> Enum.sum()|> IO.inspect(label: "part one")endend
%{"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},"mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"},}
defmodule CustomCustoms.MixProject douse Mix.Projectdef project do[app: :custom_customs,version: "0.1.0",elixir: "~> 1.10",start_permanent: Mix.env() == :prod,deps: deps()]end# Run "mix help compile.app" to learn about applications.def application do[extra_applications: [:logger]]end# Run "mix help deps" to learn about dependencies.defp deps do[# {:dep_from_hexpm, "~> 0.3.0"},{:mix_test_watch, "~> 1.0", only: :dev, runtime: false}# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}]endend
defmodule CustomCustoms do@moduledoc """Documentation for `CustomCustoms`."""@doc """Hello world.## Examplesiex> CustomCustoms.hello():world"""def hello do:worldend@spec count_group(binary) :: non_neg_integerdef count_group(input) doinput|> CustomCustoms.groups()|> unioner()|> String.length()end@spec groups(binary) :: [binary]def groups(input) doinput |> String.split("\n", trim: true)end@spec unioner(maybe_improper_list) :: binarydef unioner(inputs) when is_list(inputs) doinputs|> Enum.map(&String.graphemes/1)|> Enum.map(&MapSet.new/1)|> Enum.reduce(&MapSet.intersection/2)|> Enum.join()endend
wsqsklfrwivqhcwwgyzeanwkhfraeogtbdscwrdofujgnmydfrgodgjoqmrfyrfdgzponxmivszjcnqhdaefwvanjddvjuywnaegjwvndkboamraiyzpxngdlynzdmgkxwpaiolrurrqrxbkdvzgwuterocnzukrvtbneogdxumykaztlrodgbxwveodujhzrpsxekgbivvhkmesczrwpjcvkmrzhsewpjrptqwujkpcrjqtmzkbakjuhynvxjnpivhszgofwumcipqmtugyobxvwfhnqgcmjrrbmgjoqzuekypfwdngamsxgykwjyjunwkjkwydiwkzjyqsdczjkoixlrnyvbmgtfwoickyhaguesfrnpqpmamagnrhntjhcxbabxthacjabhtxjchjbaxtcjbctxhaobuairedlwauixvbogejdplnldciokbauemyblsdeuiazotdembualfizobpumtwsxetsaumxwhxuftswmvqglbnuhejxbglpwzyamuelbgovcqcdwjtghispmorvlumziopsvhdwtlgjuqrcrdlspoujmtwgihvcyqnojtaidspuhvmkblwercqgdpqohtufrcvlmjgswijgmbovuakiuxtgrsuypgkdiofnbwtnpcdvyokbirwgtmnybkgeortadpwidtidtaidtidtnidtryamoxgqywpznistrlfzqfidnsbyjgaxwonsyqijwxvohazfgsioxnqfwazgyryhgaeahygrewaopckqgeyrsuhqrkwaegysocuphcsohuawqegrpkyykqahecrsoupwgeswjgdxfmkvqjmtkwbvxqfedrseqkmozylatyjmaezkrlzbmkalyezmaekigylylakemzjctqlrzgwdokxnulkdanfmcinhmejkdflapmxviraylsoufvbspyxfaewiruqktdczlyhlravxgfisunxyvlusafirrsvafxliyuqlrgcjdpnyzofkxmeivkmazvnxscotrjeygdilpfzxdnogtmlqjevfkiypcpglconixjvbzkfymuezyxucssxcycsxyxscyxscyuavrcwslpdqghmenbioxjyzkjcqgrpdnymelxuzothawviblhbcumsfqrvzwnxjydpoagieqgozpicjumxwaybnrlvdhevwdegcnoqafbavbwqectgnofacqefnwvgbooeqabfwrcjlgnvancqwovegfbqvavaxbaanhxeygmoaypjdznrngzmuhqlnyujwuynwercqbolnsyhdqsnefwcnwyzowkdciuqljeggoiyjwzckludeqyuekgwjodlzicqqxkhclsgxlqckgqxkcglnwrxtshalkfqocypqugpmfvzcvzigddiqentabxumfutnmieadqbudtemqinbambqidntaeuimjpuxdlsrpmrlxsjdixjpisrdmlialpxshdmyjrsmjxzplrdieivhrugycstznxjkawqorhxwkizjeqayougtvncszbsygjuwacioxpnktqvhrecyirhogexjzkwsntuavqcsrovwjqxnhzaykteguihfdhuphjsebqgklcomtwifycqkltbwofhspugimeyjwvrekmaxicufgyjbhqopsltzqtobyikeschjmgflwupsceghujwtfkpblyqmioaaaaagskndyzmophwclqvmylhsqdcnozpkgwvsnyodvwplgmkzchqnyfjkgwielzqshoumqbpxgzlvkwrgaqomndevzcwyajltfksqztfvpyjwdrlbmqoceviqrlshezknaowvheawroznstkblqhxfswlvegjnoyrkqciblhwqneouvkrmshdpsnwqrvekolrtrsrkryzaqzkthjiqkwzjtajzktqatgzkasrojqnzikhjaqutwrwrgwrlmvpujxbczhsgfaqtfhadqpyqbkixstvjlcrmwognuezcpuznxavjpxvuzcjacjpuzaxvupzjvaxchftodjcxvbrisuybhuodryvsfjxiwtdfnwuikzlxmbjawyshkdrtwqfkwymxzrfhzrwjdhflniwwnliwnzrliubwjosjzorjozikumqlvjobwktoaufevgrbrnkbtovduhjmvbntqilzhrgpqhgzvkmtripwpsrqwgihzvtmvmgqrtpihzdvqrwipgmthszhnyzvkwgeoezhvoynkevnhkoqwzyhvznelgkoyoyekhaxviznupwqivtzgebnolkinqwtvgzopbqwinobvgzptvbiowqtpnzgulsvitcxngwzhryaebkfpsxypjlzgoihkrnubvabzgtgotxzbztgbtbglzlsrqikouybjdvawzqnchepfoiufwavmpvfpamwuwfvmpauvwumfpapmtfuawvgghenswfjhadxgnuzryboklsleobifpuqhkzcpcwfrnhsvdgejimlzoaubkvducpzwnsjmhoilgkbaerfrpbjnwvguahecofmkisdzlufbxshgloznvrwipjcqeadmkhnmscakpleigdfjozvburwouraitbgltrdmgifcxpstnyjggouatnmrvxhdcchnrmvxdxvrhcnmdmrcdvxhnmxvndhcrcufsjilngrbydkhqexkljdhfaxyguqisbrwdkgubshipmxrlqftoyjjsisijsjiisjjisprnxkmutlpdvuaeyxochzltnkmrgmxlutkpnrblxpmkrtnuamcxhzlrpgkpmowcrnrynannqfgeuynunqcfehajocjatbfmzcetfmzbesmzefbtmbztfemfztbecgrtbynxejvqmhalfhyafxecjqvrnmbltgqtghbfrmcxaevjlnyfyqjgcemhlabnrtxvhhhwhhgmpidnesyhcowqtgulfkoehmlsxqundfgbkytwahsqywtmpgazudjtpuewnhoagzsydjlmgdupwstzjahymqwrpgikdyuatzhjmsmtaduhswzjpgyyfcirgulotspbxehdzmwnkqvaqvgyhtrniemlxpbcudwsfbguysvaqwcmdjkxmgfuybtkprqacdrjmoslapxkvuhtuavxpsflhjoktrhpcjtarlsqwkoxuvjvakpxloushtriwcnsobgwcgpwcujwdiuyabzklmqsxvuxyjkvqwdszflabmiaiqwuszlxbkjydvmvysjdmqlzwxaibkumksabwzxqivujdlyaaloaaadzushfvegcopkbwoueshcbkvdpwgesbcowkgvpduhpghduksijcevywoblpuezsvdohbwckgymowmoyohmyleymomoymzctqbypdkrialjgoxsfkqpoyabcigszdrtmfljxxkfgltdipzjqysacmbrolirmkbxsafcgdozytjpqyhtvcsfaigkwrevhoqdmzphujdrlnvzyahfewvcssfzcweayhwoeshuacyfpljbrwxpcfdltixjupqonnmuptcqodlfijrpltfunoqjicdnlqctporjifudufipcdtlnojqxuuowauxycloxsvrwajqbzfkhpgmunwjlrmuoaxvkqfbpzsyhcngaqrcybjgonhkpuvmzlfxswwarfnczvokmyjgsqxhblputvzngwyqbfulxcphoskjrmayfhzawhmuazqwbjayhzwiawxetzhgawzihgsbqoynfmevkcadjzudmyofukqezgasctvnzqkmoadfnubsiveygcgezqymskdfnouvacikzgncourmyfqeavsdpzkyupyedwyprtyifxjnsprbkqywupmlwtuqxsogxzmusgwtlqqmitxvulsgwcakuxwgjmrtslqbcnkpwtmqgorsfxyhmogknxfpqwsctbrgqsracvfiobzkxnpmwkpxvpfkzxvxpkpkxxubokpsbwlytxgviueczfqakrpsmhojdfowlhcmvasbqdgptjikyxruzenowiivonqxiflysuabtiprdkzhuuudkrhoamveqlgcjwfszdgokizyvfjhaqwcmethwguqxoadznekvcjpmbfhyrsnrhdyjsfycbzrssmrykotputsnfogyicingctoysfcvnoazqpkthxwjqhsmjdlznvkwfbtqnvkhjwlstzltjzkugbqhwvnisqytxvxjyeorpcgbstaoajbxtysvtymsxjoabvjwkowojkwjkonfjvunfkvdvnfsetoetoeotezqrmchysufoyzufmqslkesgbwppgwbsblklpledllqeaearfeahaeaexkbpnxtliuekxbtieunpexbtipuknrnburkipxteligtpwhnejqbuckxhfarpblqsbaophlqrexfsdhbqfsnlparfspkvbhfpkvsbvpsfkbfbnxfknlnftlcuzownfqfynqdqfdzydqjhwgyiszvfkcdjqhmehxwjojyhbrhsujbipjolwnbcgpreptbpbzbpehrdiepnavitecpzyndaresbkaudrijpnlogpxivdaenrhhpfwjyfjyaajyfyfjrrzrrrrrrsqlcpxhtouyzwvtyxhrsbuczklidogvqaisdgcmcdsigaasigdcdcisagiswusiwsqwiaixlbtqznjcgshuoayljitqguzbchxnssuxgbcntaliqjzhfubhpeufibkaylenaizajaxbvkchavfkoshgqjujiqqjwbijiqjyqbiebaqscfvhgtuoirnjwxlzykphfjxqtuvrwgeknboipazysclqrdoqlfejdrupqxyragmnafofsgmepnamfonagpnxtgjqsbizkycithkycsjbpzxrusnfwtloepqdrkiyqkobliwsdnhwsxjqnihzkodlahdwsilnkqboduowiqcmksnlcjfthaorelpitdgbxhedtknzthvyaquvgyoklbrhedtwcjxanseaxgovbrudfnhzktjsyclwytcgjxvodwhaenlsbrkfnmhxjlfnxrxmnefcwduktfvqgnomfjwfoucpdgxqevabkrznylwhtsvhnhirkujhzngmminhkslfprfahobpksnplfknschpsifmcnhkxkoiztsqvpceywhfbnduebkpvsunzcxhmofdiqytnfpfnefnhinmuatrvygoqxfsjcvrfxtuagojyiqnhscmtifguysjoxrnqmadhcvyetihjzfkndumnejhiatzumkydzitvhkdenmjyuhinjeumtzykdekztmhndjiuyglbtefawdsycomjdjbsgnalyokcwmtemojdwysnategclbbvgcjosaytwldempevwuatlypjixqzrsmhcerwiucsaxmtvhyjzllhcxeavjmyzurtsonwipmxveyutrhslgbaczdjifwsnmyhxctuazerkwlvjirvpwjbhyfecqvxbchwiygoegwivcydbxehoylogwcevibhtxabgwphicvexyoemivwogychrbxpwghmnxtfzbylvqsakujrdoareduqjvczsmjadhzebrcumdcrumqayjzeirczsujymdeaomtyndvrhavwdhaortcrbruirepncmbyhzkaofwxvdrecwkxyhfnbmzaduovskwceyznjbfoidhamrxvulejmaipctkvuwgzqfsnorokmuaizfsrtnwlevjcgqplqkwnraeuovstmizpjcgfatsunvqljzepckrmogwifrjobteshildmxacpzlrpdbajtmhcsoexizlpvnsiomkhgxffkhjgfwyfpgykjwwajntvwdzuvtjiowjvtkefwvopmisingjtfnocqnugbvmqeoqaoscnrlkzoiopfstaqvuosoxpxhmojguvqownkfrilteaegnhfyxcszqbwvlqzoqlrvquofgqnkwhbdactmxijqijqgmnkeoxwuvbtatiubnkgwamoqvjajdkuqtimbpvonhgwmjgnyqitvwraobuksgggvspbciqmajpijumqscvaevpqzoxgtmbnstevgqbpfsnxlepnbtgsxvqqxpsnetgmvbdepavkqngtrbjxsaplrhkuiyfmxhpvwfzolsnfxtbhediuslacmyhbkufmistxihupfsmtbxzmfhipusxtbxihmtjbsufmuzqcfpmcpfzumrzfpucwzfcvmuppzcufmqqqqjbkpeiwvuejaotbcpulbakiotxdcrycobunatbmcfvhautoqpoqopwpqockpequzlfdtrbyphsdgwbznbgmpzdnbuqtywiloaeszrkghxdvfowetrsetbwciulfheaudnstmjwrpgpxkyniszuebmolhfjdgrjenrsqcupdwhmgpujhrdmcengscutnlltunccntluhdbihrdfdrfkftwxpoqmgbuilwfstpikuhqbcmxogzktqfjpxayobvuminqgtfbopkruidmxsovyuxivuxuvqxjjjjmavqarktdeaqukzaxnwcpylkgvoqmhikaqrfysxdtumlwavingkhqogbudftnxypmzrlevakjoqqafyltcembujhdjmaqtdublyfchedmhbqljfeaytucemdtqyjahucbfltcbfjlyemaqudhcpxhbjlqwarmbhlzcawpjmqygcjlwbamhqpefhlbjcmpqawvjalmbqiphwczoywyzoqvudhfxtjesibgwynaorlnuctmlvpkgoefqsihbrwyzxamgkqthideusnyohtgleqksudiynmnhgikyustdqmepuqgrdyntmhkisehlmfcirlcmfrihrlkcyfmhinimhcrlfifqdsspodiqfifdsqajpapjdramqyjeiskugnqidmekaysrgpjfunegysqnkrijluadmngdirjyksameqlutmpogzdsxryesryedpmqxzgzcxpmlsukydregdpeolsyxkwgtzijrmmefyrxdpbsvghzqafgjagfjdhrczjfebgfjergzccfmrztsygvlistujvdkkeyoahiktwuhfqasqksmthdwaruajkthusqpweprjtfahignbcklvuqwyxapxqrceguwtjnkbhyfilvdrosxgbvorvbgxxgrobvylmpryrmlpauwizmytjcsdbnvoywsmkoatibzndcjunytqecdbxhlzmiwuoarsgjvtdowcfjymsauknbizzzzzhxjiqdaveokujxqkdvheioauhxoqdueajvkituxfsusxtfuxtfsdnaiovgxlryzhqwepbmujzbgxvpaudnmwrqohejyilqaybxdnelzokvjhmwrgusiftppyoukjfretqzsdxmhwvnaszwcvfkohpuyeqntrdamxjjzkrfqvyptahxsmnowdueyepoudnfxzkwjmqvtrshapdemlqxcyvzopaubfzhuyovbpcxbcxuyvgzhopdyczxoubvpmswzovykxucpbwwwhaohkkaovkyhoaoqvakakjowrrgrclnhiwztgqydfxzrcvjgotmchepjlgkvunyfwlmruzxitixtzdrxrzitxqzitckrtrizuxgrufeslzwvxqndhimakcyjpsoaxtzmwgjdlnkpcqevrfuiybdaxjepnmzkruslyvqifwcgqjiysmvhxfurkaegzdwlpcncerjstxhanlzkqgodfuycrjsonqyuvdaelhftxzgkrksaucfxdgmoqnhytejzlgsnuojthickrealfzdyqxwmstjfqhvaplordfgnejqhkouizpvtojzucmkupvmaozjuhimrxjoqgzbwutzjoemkvztojmeuelqmorhugsxypbtzaogtlyehzpfaqxurbmseqbgrxhauymptlzsoxldewglgdxznewzexwgldldwxgebghlxviwedpgqovbkfxcizfqobpzxkctczqfxpbkohhwhhgwwgwggwgewbyvadoriwmkhfqtcznglusxpjexmwvnjpglyeocqhfarusbkzdtizlrgmsbhinxvqueojtfawdcpkypwghxcjybruidskelqmvaonftzzwqlxogbvtuafehxrhaelugfwkoztbzytuelfhoawxgbdeglztxbouawhfxuageoztfhlbwrqydeszwhuuwzydhqcersyqsdhruewzreyhqdzsuwnbomazjwwzabmjnojamzowbojawbzmwbhzfamjopxzofjkneyaedkuiiezhxcldqrasugivchplftdorunxygydxvigduyfakwjeotutewfoajykidcvudfonrqshzbgyemtkxlapfxlapvmdcqzybugnhrketsofkbersnvythqodmuaglzxpcluqerdogftkmxnyzvsbahcpfsolzkxvdtughqmabcyrpnezwayveudvpwyiztgaeyzeuawvevarwyztpkeivrucdhsawmnimspcednuvwepmnvlicsdwuicmupenvdwsfmswudvpeioncnbekcurqrnckqbeuqrlkcnbuvbuerqkcnjylehvpejvlyphypjvheglehpylvjhlvyjfepcnwjxzisphtdgveqkynjqvwdyhitocxzkpesgteywxgihcsvdkjpzqnhdiqyntzgxwejpsvkcaamacdcjnzekclihqfyrovdwgtusmeosqnkxdpcuftzhwjmrilyynimldrewskufczohqtforeinluhctqkyszmdwgxwfcasjqbnoyrvcnqjyfvwgasvfqangjcwuysenjnngnzdfknarqmlknhmarswpenjuhdkrucdjzbnutjhdxiejmtcijktnpxmepmixcjxoijmbzjqrmighxftucvebloxijamnzwyvqxtyiucdlnfbzajcntxajbfslviukzycbtxlzjfsudvinaydvftjyizuxnbacltyryykanrgxtibzsjydfpscfavzqybblasyzfcvumwfhzasbeyampozwyfbuqsihmpuwjfvygbnzevzgcwefnihymxpubshvmbuwznygpifevzdiknatrgmhyebwfoulphretmjgxwcidlhjkiedvmcglxjdmilhxewgcclhgbjndxiegnabrrnbgabywnuarlhddmqdboahddkisuyqlqsymkgluskvuiyplqxhyabuesjpdlxagrxzlaqtkepiznwbyasgyfirmnbodztsvbbbbzskcmdxeaogqbuzmbscodakqupxgeejbqblvwcsymjvwjsymbclwbmlcyjsvmwjvcybslsymvcblwjyivbaddayibvbviaydidvybaivadbyatzulnbxedjgqegbvijuzyxtqadlladebjzrtgublzdjetaugehpzmcdgtuasbkoljvejskrgaqnbltxiybviglyatsrnoiwcbrlysgvntadgsliobcrynavtpybowxjsufavcpghdfjcikxcjxkicjxmikxjkiccjxkiotzpopfhfffnndinudlnyspztvcfuzpdhtcayvydhzcvutpteylnzmflntmefzylnztyemfugwcfqzrpsdxhnyvldfosniyuwxhcvbglxvdumygwfsnchvcowxgynhusfdqwxuykodtpehbzmgsrcnyxqougapktdbhykdpbxtfquohgtubxpdygoqhkzgyomlbmoglkbzyyogbclmzqrejtivfwzubyknaplhmgcodxswitcplundkhaqxeryvbzsgfojmfvoindcxqhzabketlugxgdqizetlhvkubaofcnxciztehdlufnkvboagqhbolxizvkmctwudoarnhsjwkowdrhskghfwkyoeacfzkvieqtoagslnegtolisqrcfstlqcifgoeerjlptikwqoczdgubanxvfevaqfnxljitbkdwucozrpgojqbicpwexfglrvyastzkpzvisdhlortfqjxkwgacyebxiezsdhjhwvgtplfshioskratyqbkzlwbzakwqyltovwpzaybqktjlylakwtbqzflxozwatvkcmibjnwcxhokusjzvmbtnjmnvmtwznjklocridexufapbnztijrefwxuvkmacdplbwjtipvfzenlmcrxuadbkvpyefticjsxrzdauwnkbmlsjryquvziqvzsiuryjvyjruqsizquykrjszviiqgpeslwatumyvxrkndhcjbgcdqpktlmuzwjrvbsinhausbcltqjmvpagndkrwhihkatgblrqpidnwjumvscijrwnhlfsaxuycyzdtcyvpmqpkybtoeagtpnyvweqrmohfillmtwhfgyzneaipqyianftmwsgqlpehezfsihltawgpnbmqyfvdjdjvjybuhgeapvnkqdmscixzltowpvgfardtlbwgjyxphmqushkjxdtvqngyzariivjpbamlvyowxsgkzincdwijheuxmkbstqpnvozapnfbzdmysukvljfzlksnydpjumvbdjfmbqnvyzskpluylxfuzsdryxvfrzlkusyxlsfwrzuyulsfxzryrzuxlfdscqqcpghyadprtkistykpiharsdsdyahrtpkipakifrhdystritysadhkptdnuxmkhcveqgwasyrvmhctngxuawsrqykbedxdhktspfgfgozqnudfowzegaxkvulniypoezgdmchtkgeponhxactvylzdmifdkotcgiwhnljbqmepazyrvxudhtkyexzncopglmviaolhadkzxiepngvytcmcxilwnkmdatosvsaovmlncxiktwdswdmlnaivkotcxkdclnvtsiaoxwmvmndksiawtxolcplaifgjtxzvfsaiykgrhvgzafikhsyrfaghrvizsykbvsldjymgzcqokiezstldyexgrwokvimjqboqjgalksbymizcedvthkilpuasvqhltgvbhrqewyxjbgejxvhjsvibeagxgeftjxbvhwexvgjbwhywjaichxrfvqlnugcesmtcmoeetkbcvlitomyavmiplyoaamvliyoayvlmioovfdmajylipxeqymbdtfetymxnzsogerunlazpijskqfbmtdywvorgudspwmfzvyljniakqebjkruynsdbcxifelogmpqvwzaauprhvlodbwmqikgsjyezfnkjmehoatdxslfgrwynidzrnowjumiveqaltpfkcjwmktnafrdieolzhupdappjxhzjqrwqrzjahbvjizfyqwxpgcbziectegxwvhegxcqtwfzmwjpnviqdenpgijdvhzmwqzwinjpqvmhdtmwzjdpiqnvthoezprianclgudvksyfbwgizdnfqwvebathopyurklqeipwgrbvfndjtazhlokyuvodgpzryhxwtlenkfuiabtufkbyhegznadjilwvoprdbvigopmznrclaexjfwkyhtqkbvgmrioznjfycapxwleqdhzrqgpeasymbhokunvjxdilwcfahmsunwpjzrtdlxqeymjuqrshdatxnpzelywnrjqamdstpxlehwzuyqyxbahsrlmvzwuedjntphudayszfmxptwljrqnezdanjmanqwjedanzdjqdnwafcaxdbnrabxtbrtaarbttarbajoshwczdmxkrivpfxpskmtchuzrapmyhacuzrxgktsbskzphxmcraelxxmoayomayizocxhdkenqyavbihlpsrnkfotwnpikhoftpahurnomkcvdzilyckapvofuzdlybtnrmneglmcpyrdzjavfutowkcdlnuqbvfejaovdsocqlfebxnauenjduqfcavblogmyfnhbemdjizhyihyzzhyiijzhyyhzijxzchrsbxhivegaxdnsfjopyxxuyxexxxnyhlqrtzipagkmowuefvbcdewhoialgqyfpmcnrdkzjroqhlinpdfwycmkgzaseytwerhutgwoirnewxsmxnjpohdmgykbtsraiqfusjphqtbuoankirgxdymfamqpdilhfsytgoxbujknrfsxordjuayhgkpiqbmntipsvgmzyaqfdrtkuhonjbxxwekvftlbnujarmjweukmvanrbxltfluebtmwjranfkvjfuvklrbnmtwaerlhbvemkjfanwutpidycjeznrvhvcjbepznrywgnjvyofcaiplzerermiyzpvjcnhpxrjqetknzvsycqnxmivdaxvdqimqvxdmivxmqidvxdmqiwfapsgscwgdufrosfwgzfayobrcfyzobarczobcryafrfboyczamusbrjnygkftpoxadsfnjgmoutkxrbyacnljabtukighmrqyxosfhwqxnwnhxqxbqhwvgqvvvqvlwovwawusiwecvdzvauynewjdhtlcmnpydwjmehlclllllfmzgqbawtycdnzhfjvixwgnidzfxtajqychvbdfqzjwvanycxtighbndgfatjwixvqhyczblvsftmmtvlsfltvsfmfvltsmtlsmvfwdvhntmbcausfgykmcanyusbkvgftdwmauygksiwvftbcdncftwojgesayvkmudbnkvdbcwafyzmnugsqtabhngwavuprxqlitpitqzrfeatyrpmsiiroptwkuxtxgdtoobjuagbxqkwitwvthxnqgakihjqikngxawodarkwgqpiwqagkiatobwlprngejvmixdqcuofwbtepxrqgimuyhdajklhqeotifmpnvuyasqzdlxrpbdxzqrplrhqzpldxqxpzrlbdwppgdyvphwppnnetpfbljariowpcefadnkhzvkgdhaupicxnezfeadohrcjnkgiypsmtlfvzuqhzaimfoqcuxtpdveykrgjaytcjiumfpkdvgrehozqbtquhdlgfrmejkoiyvpzacqcvkdrywezfmtiujaoghpmsueprdbjnzflgvytcwoglftyscmvbojzudwrpenjlgyfsntvwmrzdcuopbeosivwyfumczldjbgretqphntuikjgnmibokjhbjiklbzgymjndndbygmzjyjnhbdmzjnyzbdmgibndyztmjiorhgcxbfjamwyenpvdksquevytoqjxznkbadhrmisgfcwwbengjckvhmasfiyqrodxesdcnwxjohmqkrgiybfavohkxbjgyvdeiqnsrcmawfmkqgwxicpqvailgsnxymoxqrmidggqxihmxqmigrpzecksafgljlazcgfekugclqeakzxrfcgkoflzaeeawcgkfzllehtfqcxwojlmpdubnsjyrcmahmynajrshbcnbrhscamyjjymhabnrsccybjsrnamhuzybinvzvblpyxvlmybzaeuyibogwkxpvjtfismchnrzdqwphgsnrqoupzitwafvxekhurvxiqezgknofaptwswagytrequfixhpvzonskopknhiteqjwzxlfauvgrsrghduvaijsyblfboybodyazhcqrkkmqfcuazmickatzqhgqaosvkdzpcbcazkgwhtqsnpgwemqclydgfjhervgeubxjerkbigooeigaxlhsifzcgizhcfglsishgzlcfmsfqhtecuibyggdwkvyiqyroxlivgqpwftdvlgcubjtsafjbwuiyxlqmrnsvibpqzeymurdcbtukihfeqjrnvuchikvqtebrjgszxqvyfrakplaslgzyfxpjvkrqsgyjaxzqvlfkpryprgqvkxjsflzablxpvxvlqsplvbxpbwuuwbkuctazmatcuzktuqkczadcupkztaiupzckatvirjwanfcxdsqkyzthmubshtdubncmkayzfqwjxrisdrvpcqybwomzahmvhskwswhemlvwmvshhsmwlvwrivxopdhgfauslbevnfeoxdwrmiqlyapcbsgudolivuprewxazsbgfzudaxgosplbvfiweroxiglrthpasvfwudebgcjihxvtazydernlexdhjtgacvryuxqjhgystbrdaepcvdcfryegvmjaxthznnyxnaunuvsezitjwlmidztxjcesgrwyulwizjsetcpjncjnugnjnjicjwnkmzjoeqhsvdgtadtzarrtdzbtzrdduzrtnkmszydrtxvrpjvoqvkyjpvemjppjvmjtvpjqbywgpexdrceihgzxtrknslzukegspqxhridyfusfwpaihyjkxzerdqzyfriudebqxpshknzsiehmplbjgfqwegwziblfhsqmjpnzslbnghpjfqiewmwgniseplhzfqbjmgaznqpwsembihlfjdknijhdxedlevcfzpjatlwtaclzpjwfcltjfzpawfptzjclawtfcpwlzsajniindjnpydcdynjpcjpcyndncdjypwuilxjabeftcroshpkudnygvmrqyqzhalsjfazutsmkqqaszrqmrzsxawjvwxjvwjvwjvesgpbowepogwbsoewpsbgwpgqmscobeewsobpgbbbwrgzjvrgpatzrgkrtegfrxgkbcotpdrwmudwkmpbtcurrdzyjbvsmuoacwkgfhciwsnzamkugpdybrvoxmwpgvzvwwbyktilsjuemvqpdornczhxkyfzbqitgahrvsuocjpledmnqhytnurliebszcdvmpojkohxsjvdzqabegpoqvgxrjbtelpuwgqevkbpjnxoifcjsuviaqasvcfjuiqaqcfsujivsicfaqjvuvaiqusfcjicjtklarqhsmdyqcamftiujyszibcntjyasgoxeqmwvyitlamrqsujpchnmxwtudcsbpryzdijaguqvfkotwqousdgyknvrmxfpijbzehixgmxivxiwwuwgweenktazsgmqvpkxtcvsaznmpgxmkmmxntfkyoarlejixbdcolnibtxeryjadfkyagxfwbisvhurjmpkqtjfqhmekxduwarixhwfrdmqjuiakelajdxeuikmhrqfwihdkmeafjqwurxotjeuvbqshdncxapmlcvukgsexriyabhqbnvxquaeodtwcshffgeuoecmjflpwjlwfpimfwmlqpjjpfmwlkvpjcbfaihnusglrrbvpcsfyihnjazguxlkqkkpignpkghajppqpzomcbqbohstryufljioenypszgxrdhberxeyhpnsdzgbepghbynszrxdazlbegicyviavnezslgymtkefoasrunwvzluerpwqcidxbhgjhxqgtgthqxpocvszotkwzpvszvospvopcnzslvzmpnfosrzzgrhrvzerszuebwmzkpurbtkdmeyumskvjibdddndkdarcnphkouwrtuexkorhnslcrtwyudpbxgvhtsrvqelyagudwbpomtubsahrkvpeyqwdlgfrrrrrxptijvhfaxpricabvtfhjvjpaxfhtiahtxfvjpivhftijpxabxshqfzgyrbzgfxsrqyhszrqhgfxbytxbczyhfqsrgybfhsqzxrgtaevdznpkfezadwvfnkpltkdpnzafvtetvfdknpeazeaaaaadzwyofjxnugvbiparuwrfvygonibzadxjpafcmcmaffacmlcisgdjbkcidyydjgyenyltfbvzwocksbzfstkvjcoykzuvsobxficthnkftzogvcbsvszjkobtfygcheszpuhzepxzehpshepzbymelzzhqswvhlzgcetbkuwzeopmnbjwfaeqmkzmqaufjvwlqlfuvirnjwyvuwfglxejztgkylzrlwdiykgzuloiwargvxkevxcygjrvnwbxubvpugixnwjuwrmjvngbxgcnvwbxujjwnuxbvgqqqqewqzwynrvuvxym
#!/usr/bin/env bash# Start the program in the backgroundexec "$@" &pid1=$!# Silence warnings from here onexec >/dev/null 2>&1# Read from stdin in the background and# kill running program when stdin closesexec 0<&0 $(while read; do :; donekill -KILL $pid1) &pid2=$!# Clean upwait $pid1ret=$?kill -KILL $pid2exit $ret
defmodule MixTestWatch.Mixfile douse Mix.Project@version "1.0.2"def project do[app: :mix_test_watch,version: @version,elixir: "~> 1.0",build_embedded: Mix.env() == :prod,start_permanent: Mix.env() == :prod,deps: deps(),name: "mix test.watch",description: "Automatically run tests when files change",package: [maintainers: ["Louis Pilfold"],licenses: ["MIT"],links: %{"GitHub" => "https://github.com/lpil/mix-test.watch"},files: ~w(LICENCE README.md CHANGELOG.md lib priv mix.exs)]]enddef application do[mod: {MixTestWatch, []}, applications: [:file_system]]enddefp deps do# File system event watcher[{:file_system, "~> 0.2.1 or ~> 0.3"},# App env state test helper{:temporary_env, "~> 2.0", only: :test},# Documentation generator{:ex_doc, ">= 0.12.0", only: :dev}]endend
defmodule MixTestWatch do@moduledoc """Automatically run your Elixir project's tests each time you save a file.Because TDD is awesome."""use Applicationalias MixTestWatch.Watcher## Public interface#@spec run([String.t()]) :: no_returndef run(args \\ []) when is_list(args) doMix.env(:test)put_config(args):ok = Application.ensure_started(:file_system):ok = Application.ensure_started(:mix_test_watch)Watcher.run_tasks()no_halt_unless_in_repl()end## Application callback#def start(_type, _args) doimport Supervisor.Spec, warn: falsechildren = [worker(Watcher, [])]opts = [strategy: :one_for_one, name: Sup.Supervisor]Supervisor.start_link(children, opts)end## Internal functions#defp put_config(args) doconfig = MixTestWatch.Config.new(args)Application.put_env(:mix_test_watch, :__config__, config, persistent: true)enddefp no_halt_unless_in_repl dounless Code.ensure_loaded?(IEx) && IEx.started?() do:timer.sleep(:infinity)endendend
defmodule MixTestWatch.Watcher douse GenServeralias MixTestWatch, as: MTWalias MixTestWatch.Configrequire Logger@moduledoc """A server that runs tests whenever source files change."""## Client API#def start_link doGenServer.start_link(__MODULE__, [], name: __MODULE__)enddef run_tasks doGenServer.cast(__MODULE__, :run_tasks)end## Genserver callbacks#@spec init(String.t()) :: {:ok, %{args: String.t()}}def init(_) doopts = [dirs: [Path.absname("")], name: :mix_test_watcher]case FileSystem.start_link(opts) do{:ok, _} ->FileSystem.subscribe(:mix_test_watcher){:ok, []}other ->Logger.warn """Could not start the file system monitor."""otherendenddef handle_cast(:run_tasks, state) doconfig = get_config()MTW.Runner.run(config){:noreply, state}enddef handle_info({:file_event, _, {path, _events}}, state) doconfig = get_config()path = to_string(path)if MTW.Path.watching?(path, config) doMTW.Runner.run(config)MTW.MessageInbox.flush()end{:noreply, state}end## Internal functions#@spec get_config() :: %Config{}defp get_config doApplication.get_env(:mix_test_watch, :__config__, %Config{})endend
defmodule MixTestWatch.Runner do@moduledoc falsealias MixTestWatch.Config## Behaviour specification#@callback run(Config.t()) :: :ok## Public API#@doc """Run tests using the runner from the config."""def run(%Config{} = config) do:ok = maybe_clear_terminal(config)IO.puts("\nRunning tests..."):ok = maybe_print_timestamp(config):ok = config.runner.run(config):okend## Internal functions#defp maybe_clear_terminal(%{clear: false}), do: :okdefp maybe_clear_terminal(%{clear: true}), do: :ok = IO.puts(IO.ANSI.clear() <> IO.ANSI.home())defp maybe_print_timestamp(%{timestamp: false}), do: :okdefp maybe_print_timestamp(%{timestamp: true}) do:ok =DateTime.utc_now()|> DateTime.to_string()|> IO.puts()endend
defmodule MixTestWatch.PortRunner do@moduledoc """Run the tasks in a new OS process via ports"""alias MixTestWatch.Config@doc """Run tests using the runner from the config."""def run(%Config{} = config) docommand = build_tasks_cmds(config)case :os.type() do{:win32, _} ->System.cmd("cmd", ["/C", "set MIX_ENV=test&& mix test"], into: IO.stream(:stdio, :line))_ ->Path.join(:code.priv_dir(:mix_test_watch), "zombie_killer")|> System.cmd(["sh", "-c", command], into: IO.stream(:stdio, :line))end:okend@doc """Build a shell command that runs the desired mix task(s).Colour is forced on- normally Elixir would not print ANSI colours whilerunning inside a port."""def build_tasks_cmds(config = %Config{}) doconfig.tasks|> Enum.map(&task_command(&1, config))|> Enum.join(" && ")enddefp task_command(task, config) doargs = Enum.join(config.cli_args, " ")ansi =case Enum.member?(config.cli_args, "--no-start") dotrue -> "run --no-start -e 'Application.put_env(:elixir, :ansi_enabled, true);'"false -> "run -e 'Application.put_env(:elixir, :ansi_enabled, true);'"end[config.cli_executable, "do", ansi <> ",", task, args]|> Enum.filter(& &1)|> Enum.join(" ")|> (fn command -> "MIX_ENV=test #{command}" end).()|> String.trim()endend
defmodule MixTestWatch.Path do@moduledoc """Decides if we should refresh for a path."""alias MixTestWatch.Config@elixir_source_endings ~w(.erl .ex .exs .eex .leex .xrl .yrl .hrl)@ignored_dirs ~w(deps/ _build/)## Public API#@spec watching?(MixTestWatch.Config.t(), String.t()) :: booleandef watching?(path, config \\ %Config{}) dowatched_directory?(path) and elixir_extension?(path, config.extra_extensions) andnot excluded?(config, path)end## Internal functions#@spec excluded?(MixTestWatch.Config.t(), String.t()) :: booleandefp excluded?(config, path) doconfig.exclude|> Enum.map(fn pattern -> Regex.match?(pattern, path) end)|> Enum.any?()enddefp watched_directory?(path) donot String.starts_with?(path, @ignored_dirs)enddefp elixir_extension?(path, extra_extensions) doString.ends_with?(path, @elixir_source_endings ++ extra_extensions)endend
defmodule MixTestWatch.MessageInbox do@moduledoc """Helpers for managing process messages."""@spec flush :: :ok@doc """Clear the process inbox of all messages."""def flush doreceive do_ -> flush()after0 -> :okendendend
defmodule MixTestWatch.Config do@moduledoc """Responsible for gathering and packaging the configuration for the task."""@default_runner MixTestWatch.PortRunner@default_tasks ~w(test)@default_clear false@default_timestamp false@default_exclude [~r/\.#/, ~r{priv/repo/migrations}]@default_extra_extensions []@default_cli_executable "mix"defstruct tasks: @default_tasks,clear: @default_clear,timestamp: @default_timestamp,runner: @default_runner,exclude: @default_exclude,extra_extensions: @default_extra_extensions,cli_executable: @default_cli_executable,cli_args: []@spec new([String.t()]) :: %__MODULE__{}@doc """Create a new config struct, taking values from the ENV"""def new(cli_args \\ []) do%__MODULE__{tasks: get_tasks(),clear: get_clear(),timestamp: get_timestamp(),runner: get_runner(),exclude: get_excluded(),cli_executable: get_cli_executable(),cli_args: cli_args,extra_extensions: get_extra_extensions()}enddefp get_runner doApplication.get_env(:mix_test_watch, :runner, @default_runner)enddefp get_tasks doApplication.get_env(:mix_test_watch, :tasks, @default_tasks)enddefp get_clear doApplication.get_env(:mix_test_watch, :clear, @default_clear)enddefp get_timestamp doApplication.get_env(:mix_test_watch, :timestamp, @default_timestamp)enddefp get_excluded doApplication.get_env(:mix_test_watch, :exclude, @default_exclude)enddefp get_cli_executable doApplication.get_env(:mix_test_watch, :cli_executable, @default_cli_executable)enddefp get_extra_extensions doApplication.get_env(:mix_test_watch, :extra_extensions, @default_extra_extensions)endend
defmodule Mix.Tasks.Test.Watch douse Mix.Task@moduledoc """A task for running tests whenever source files change."""@shortdoc "Automatically run tests on file changes"@preferred_cli_env :testdefdelegate run(args), to: MixTestWatchend
{<<"app">>,<<"mix_test_watch">>}.{<<"build_tools">>,[<<"mix">>]}.{<<"description">>,<<"Automatically run tests when files change">>}.{<<"elixir">>,<<"~> 1.0">>}.{<<"files">>,[<<"LICENCE">>,<<"README.md">>,<<"CHANGELOG.md">>,<<"lib">>,<<"lib/mix">>,<<"lib/mix/tasks">>,<<"lib/mix/tasks/test">>,<<"lib/mix/tasks/test/watch.ex">>,<<"lib/mix_test_watch.ex">>,<<"lib/mix_test_watch">>,<<"lib/mix_test_watch/config.ex">>,<<"lib/mix_test_watch/message_inbox.ex">>,<<"lib/mix_test_watch/path.ex">>,<<"lib/mix_test_watch/port_runner">>,<<"lib/mix_test_watch/port_runner/port_runner.ex">>,<<"lib/mix_test_watch/runner.ex">>,<<"lib/mix_test_watch/watcher.ex">>,<<"priv">>,<<"priv/zombie_killer">>,<<"mix.exs">>]}.{<<"licenses">>,[<<"MIT">>]}.{<<"links">>,[{<<"GitHub">>,<<"https://github.com/lpil/mix-test.watch">>}]}.{<<"name">>,<<"mix_test_watch">>}.{<<"requirements">>,[[{<<"app">>,<<"file_system">>},{<<"name">>,<<"file_system">>},{<<"optional">>,false},{<<"repository">>,<<"hexpm">>},{<<"requirement">>,<<"~> 0.2.1 or ~> 0.3">>}]]}.{<<"version">>,<<"1.0.2">>}.
mix test.watch==============[](https://travis-ci.org/lpil/mix-test.watch)[](https://hex.pm/packages/mix_test_watch)[](https://hex.pm/packages/mix_test_watch)Automatically run your Elixir project's tests each time you save a file.Because TDD is awesome.## UsageAdd it to your dependencies:```elixir# mix.exs (Elixir 1.4)def deps do[{:mix_test_watch, "~> 1.0", only: :dev, runtime: false}]end``````elixir# mix.exs (Elixir 1.3 and earlier)def deps do[{:mix_test_watch, "~> 1.0", only: :dev}]end```Run the mix task```mix test.watch```Start hacking :)## Running Additional Mix TasksThrough the mix config it is possible to run other mix tasks as well as thetest task. For example, if I wished to run the [Dogma][dogma] code stylelinter after my tests I would do so like this.[dogma]: https://github.com/lpil/dogma```elixir# config/config.exsuse Mix.Configif Mix.env == :dev doconfig :mix_test_watch,tasks: ["test","dogma",]end```Tasks are run in the order they appear in the list, and the progression willstop if any command returns a non-zero exit code.All tasks are run with `MIX_ENV` set to `test`.## Passing Arguments To TasksAny command line arguments passed to the `test.watch` task will be passedthrough to the tasks being run. If I only want to run the tests from one fileevery time I save a file I could do so with this command:```mix test.watch test/file/to_test.exs```Note that if you have configured more than one task to be run these argumentswill be passed to all the tasks run, not just the test command.## Running tests of modules that changedElixir 1.3 introduced `--stale` option that will run only those test files which reference modules that have changed since the last run. You can pass it to test.watch:```mix test.watch --stale```## Clearing The Console Before Each RunIf you want mix test.watch to clear the console before each run, you canenable this option in your config/dev.exs as follows:```elixir# config/config.exsuse Mix.Configif Mix.env == :dev doconfig :mix_test_watch,clear: trueend```## Excluding files or directoriesTo ignore changes from specific files or directories just add `exclude:` regexppatterns to your config in `mix.exs`:```elixir# config/config.exsuse Mix.Configif Mix.env == :dev doconfig :mix_test_watch,exclude: [~r/db_migration\/.*/,~r/useless_.*\.exs/]end```The default is `exclude: [~r/\.#/, ~r{priv/repo/migrations}]`.## Compatibility NotesOn Linux you may need to install `inotify-tools`.## Desktop NotificationsYou can enable desktop notifications with[ex_unit_notifier](https://github.com/navinpeiris/ex_unit_notifier).## Licence```mix test.watchCopyright © 2015-present Louis PilfoldPermission is hereby granted, free of charge, to any person obtaininga copy of this software and associated documentation files (the "Software"),to deal in the Software without restriction, including without limitationthe rights to use, copy, modify, merge, publish, distribute, sublicense,and/or sell copies of the Software, and to permit persons to whom theSoftware is furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be includedin all copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIESOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWAREOR THE USE OR OTHER DEALINGS IN THE SOFTWARE.```
The MIT License (MIT)mix test.watchCopyright © 2015-present Louis Pilfold. All Rights Reserved.Permission is hereby granted, free of charge, to any person obtaining a copyof this software and associated documentation files (the "Software"), to dealin the Software without restriction, including without limitation the rightsto use, copy, modify, merge, publish, distribute, sublicense, and/or sellcopies of the Software, and to permit persons to whom the Software isfurnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included inall copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS INTHE SOFTWARE.
Changelog=========## v1.0.2 - 2019-11-17- Zombie killer script is run with bash to avoid platform specific issues withsh implementations.## v1.0.1 - 2019-10-25- Include zombie killer script in hex package.## v1.0.0 - 2019-10-25- LiveView templates are now watched.## v0.9.0 - 2018-09-17- Avoid starting application if `--no-start` is given.- Hot runner removed.## v0.8.0 - 2018-07-30- Application started on test run. Revert of v0.7 behaviour.## v0.7.0 - 2018-07-35- No longer start application on test run.- Do not watch the Ecto migration directory by default.## v0.6.0 - 2018-03-27- Switch from `fs` to `file_system` for file system event watching.## v0.5.0 - 2017-08-26- Windows support (Rustam @rustamtolipov)## v0.4.1 - 2017-06-21- Revert to `fs` v0.9.1 to maintain Phoenix Live Reload compatibility.https://github.com/phoenixframework/phoenix_live_reload/commit/e54bf6fb301436797ff589e0b76a047bb79b6870## v0.4.0 - 2017-04-22- Emacs temporary files can no longer trigger a test run.## v0.3.3 - 2017-02-08- Fixed a bug where arguments were not being correctly passed to thetest running BEAM instance.## v0.3.1 - 2017-02-04- Fixed race condition bug on OSX where tests would fail to run whenfiles are changed.## v0.3.0 - 2017-01-29- Test runs optionally print a timestamp (Scotty @unclesnottie)- Paths can be ignored by watcher (Alex Myasoedov @msoedov)- Paths can be ignored by watcher (Alex Myasoedov @msoedov)- Ability to specify additional watched file extensions. (Dave Shah @daveshah)- Erlang `.hrl` header files are now watched.- The existing VM can now reused for running the tests with the HotRunner.This gives us Windows support and a performance increase.Sadly it cannot be used as the default due to a bug in the Elixir compiler.## v0.2.6 - 2016-02-28- The terminal can now be cleared between test runs.(Gerard de Brieder @smeevil)## v0.2.5 - 2015-12-31- It is now possible to run addition tasks using mix config.- Erlang `.xrl` and `.yrl` files are watched. (John Hamelink @johnhamelink)- The shell command used to run the tasks can be specified (i.e. `iex -S`).(John Hamelink @johnhamelink)- Command line arguments are forwarded to tasks being run. (Johan Lind @behe)## v0.2.3 - 2015-08-23- The `_build` directory is ignored, as well as `deps/`.- Erlang `.erl` files are now watched.## v0.2.2 - 2015-08-22- Tests now run once immediately after running the mix task. (Johan Lind @behe)- Porcelain dependancy removed.- Switched from bash to sh for running shell commands.
defmodule FileSystem.Mixfile douse Mix.Projectdef project do[ app: :file_system,version: "0.2.10",elixir: "~> 1.3",deps: deps(),description: "A file system change watcher wrapper based on [fs](https://github.com/synrc/fs)",source_url: "https://github.com/falood/file_system",package: package(),compilers: [:file_system | Mix.compilers()],aliases: ["compile.file_system": &file_system/1],docs: [extras: ["README.md"],main: "readme",]]enddef application do[applications: [:logger],]enddefp deps do[{ :ex_doc, "~> 0.14", only: :docs },]enddefp file_system(_args) docase :os.type() do{:unix, :darwin} -> compile_mac()_ -> :okendenddefp compile_mac dorequire Loggersource = "c_src/mac/*.c"target = "priv/mac_listener"if Mix.Utils.stale?(Path.wildcard(source), [target]) doLogger.info "Compiling file system watcher for Mac..."cmd = "clang -framework CoreFoundation -framework CoreServices -Wno-deprecated-declarations #{source} -o #{target}"if Mix.shell().cmd(cmd) > 0 doLogger.error "Could not compile file system watcher for Mac, try to run #{inspect cmd} manually inside the dependency."elseLogger.info "Done."end:okelse:noopendenddefp package do%{ maintainers: ["Xiangrong Hao", "Max Veytsman"],files: ["lib", "README.md", "mix.exs","c_src/mac/cli.c","c_src/mac/cli.h","c_src/mac/common.h","c_src/mac/compat.c","c_src/mac/compat.h","c_src/mac/main.c","priv/inotifywait.exe",],licenses: ["WTFPL"],links: %{"Github" => "https://github.com/falood/file_system"}}endend
defmodule FileSystem do@moduledoc File.read!("README.md")@doc """## Options* `:dirs` ([string], required), the dir list to monitor* `:backend` (atom, optional), default backends: `:fs_mac`for `macos`, `:fs_inotify` for `linux`, `freebsd` and `openbsd`,`:fs_windows` for `windows`* `:name` (atom, optional), `name` can be used to subscribe asthe same as pid when the `name` is given. The `name` shouldbe the name of worker process.* All rest options will treated as backend options. See backendmodule documents for more details.## ExampleSimple usage:iex> {:ok, pid} = FileSystem.start_link(dirs: ["/tmp/fs"])iex> FileSystem.subscribe(pid)Get instant notifications on file changes for Mac OS X:iex> FileSystem.start_link(dirs: ["/path/to/some/files"], latency: 0)Named monitor with specified backend:iex> FileSystem.start_link(backend: :fs_mac, dirs: ["/tmp/fs"], name: :worker)iex> FileSystem.subscribe(:worker)"""@spec start_link(Keyword.t) :: GenServer.on_start()def start_link(options) doFileSystem.Worker.start_link(options)end@doc """Register the current process as a subscriber of a file_system worker.The pid you subscribed from will now receive messages like{:file_event, worker_pid, {file_path, events}}{:file_event, worker_pid, :stop}"""@spec subscribe(GenServer.server) :: :okdef subscribe(pid) doGenServer.call(pid, :subscribe)endend
defmodule FileSystem.Worker do@moduledoc """FileSystem Worker Process with the backend GenServer, receive events from Port Processand forward it to subscribers."""use GenServer@doc falsedef start_link(args) do{opts, args} = Keyword.split(args, [:name])GenServer.start_link(__MODULE__, args, opts)end@doc falsedef init(args) do{backend, rest} = Keyword.pop(args, :backend)with {:ok, backend} <- FileSystem.Backend.backend(backend),{:ok, backend_pid} <- backend.start_link([{:worker_pid, self()} | rest])do{:ok, %{backend_pid: backend_pid, subscribers: %{}}}else_ -> :ignoreendend@doc falsedef handle_call(:subscribe, {pid, _}, state) doref = Process.monitor(pid)state = put_in(state, [:subscribers, ref], pid){:reply, :ok, state}end@doc falsedef handle_info({:backend_file_event, backend_pid, file_event}, %{backend_pid: backend_pid}=state) dostate.subscribers |> Enum.each(fn {_ref, subscriber_pid} ->send(subscriber_pid, {:file_event, self(), file_event})end){:noreply, state}enddef handle_info({:DOWN, ref, _, _pid, _reason}, state) dosubscribers = Map.drop(state.subscribers, [ref]){:noreply, %{state | subscribers: subscribers}}enddef handle_info(_, state) do{:noreply, state}endend
require Loggerdefmodule FileSystem.Backends.FSWindows do@moduledoc """This file is a fork from https://github.com/synrc/fs.FileSysetm backend for windows, a GenServer receive data from Port, parse eventand send it to the worker process.Need binary executable file packaged in to use this backend.## Backend Options* `:recursive` (bool, default: true), monitor directories and their contents recursively## Executable File PathThe default executable file is `inotifywait.exe` in `priv` dir of `:file_system` application, there're two ways to custom it, useful when run `:file_system` with escript.* config with `config.exs``config :file_system, :fs_windows, executable_file: "YOUR_EXECUTABLE_FILE_PATH"`* config with `FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE` os environmentFILESYSTEM_FSWINDOWS_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH"""use GenServer@behaviour FileSystem.Backend@sep_char <<1>>@default_exec_file "inotifywait.exe"def bootstrap doexec_file = executable_path()if not is_nil(exec_file) and File.exists?(exec_file) do:okelseLogger.error "Can't find executable `inotifywait.exe`"{:error, :fs_windows_bootstrap_error}endenddef supported_systems do[{:win32, :nt}]enddef known_events do[:created, :modified, :removed, :renamed, :undefined]enddefp executable_path doexecutable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || executable_path(:priv)enddefp executable_path(:config) doApplication.get_env(:file_system, :fs_windows)[:executable_file]enddefp executable_path(:system_env) doSystem.get_env("FILESYSTEM_FSMWINDOWS_EXECUTABLE_FILE")enddefp executable_path(:system_path) doSystem.find_executable(@default_exec_file)enddefp executable_path(:priv) docase :code.priv_dir(:file_system) do{:error, _} ->Logger.error "`priv` dir for `:file_system` application is not avalible in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSWINDOWS_EXECUTABLE_FILE` env."nildir when is_list(dir) ->Path.join(dir, @default_exec_file)endenddef parse_options(options) docase Keyword.pop(options, :dirs) do{nil, _} ->Logger.error "required argument `dirs` is missing"{:error, :missing_dirs_argument}{dirs, rest} ->format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlistargs = ['--format', format, '--quiet', '-m', '-r'| dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)]parse_options(rest, args)endenddefp parse_options([], result), do: {:ok, result}defp parse_options([{:recursive, true} | t], result) doparse_options(t, result)enddefp parse_options([{:recursive, false} | t], result) doparse_options(t, result -- ['-r'])enddefp parse_options([{:recursive, value} | t], result) doLogger.error "unknown value `#{inspect value}` for recursive, ignore"parse_options(t, result)enddefp parse_options([h | t], result) doLogger.error "unknown option `#{inspect h}`, ignore"parse_options(t, result)enddef start_link(args) doGenServer.start_link(__MODULE__, args, [])enddef init(args) do{worker_pid, rest} = Keyword.pop(args, :worker_pid)case parse_options(rest) do{:ok, port_args} ->port = Port.open({:spawn_executable, to_charlist(executable_path())},[:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}])Process.link(port)Process.flag(:trap_exit, true){:ok, %{port: port, worker_pid: worker_pid}}{:error, _} ->:ignoreendenddef handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do{file_path, events} = line |> parse_linesend(state.worker_pid, {:backend_file_event, self(), {file_path, events}}){:noreply, state}enddef handle_info({port, {:exit_status, _}}, %{port: port}=state) dosend(state.worker_pid, {:backend_file_event, self(), :stop}){:stop, :normal, state}enddef handle_info({:EXIT, port, _reason}, %{port: port}=state) dosend(state.worker_pid, {:backend_file_event, self(), :stop}){:stop, :normal, state}enddef handle_info(_, state) do{:noreply, state}enddef parse_line(line) do{path, flags} =case line |> to_string |> String.split(@sep_char, trim: true) do[dir, flags, file] -> {Enum.join([dir, file], "\\"), flags}[path, flags] -> {path, flags}end{path |> Path.split() |> Path.join(), flags |> String.split(",") |> Enum.map(&convert_flag/1)}enddefp convert_flag("CREATE"), do: :createddefp convert_flag("MODIFY"), do: :modifieddefp convert_flag("DELETE"), do: :removeddefp convert_flag("MOVED_TO"), do: :renameddefp convert_flag(_), do: :undefinedend
require Loggerdefmodule FileSystem.Backends.FSPoll do@moduledoc """FileSysetm backend for any OS, a GenServer that regularly scans file system todetect changes and send them to the worker process.## Backend Options* `:interval` (integer, default: 1000), polling interval## Use FSPoll BackendUnlike other backends, polling backend is never automatically chosen in anyOS environment, despite being usable on all platforms.To use polling backend, one has to explicitly specify in the backend option."""use GenServer@behaviour FileSystem.Backenddef bootstrap, do: :okdef supported_systems do[{:unix, :linux}, {:unix, :freebsd}, {:unix, :openbsd}, {:unix, :darwin}, {:win32, :nt}]enddef known_events do[:created, :deleted, :modified]enddef start_link(args) doGenServer.start_link(__MODULE__, args, [])enddef init(args) doworker_pid = Keyword.fetch!(args, :worker_pid)dirs = Keyword.fetch!(args, :dirs)interval = Keyword.get(args, :interval, 1000)Logger.info("Polling file changes every #{interval}ms...")send(self(), :first_check){:ok, {worker_pid, dirs, interval, %{}}}enddef handle_info(:first_check, {worker_pid, dirs, interval, _empty_map}) doschedule_check(interval){:noreply, {worker_pid, dirs, interval, files_mtimes(dirs)}}enddef handle_info(:check, {worker_pid, dirs, interval, stale_mtimes}) dofresh_mtimes = files_mtimes(dirs)diff(stale_mtimes, fresh_mtimes)|> Tuple.to_list|> Enum.zip([:created, :deleted, :modified])|> Enum.each(&report_change(&1, worker_pid))schedule_check(interval){:noreply, {worker_pid, dirs, interval, fresh_mtimes}}enddefp schedule_check(interval) doProcess.send_after(self(), :check, interval)enddefp files_mtimes(dirs, files_mtimes_map \\ %{}) doEnum.reduce(dirs, files_mtimes_map, fn dir, map ->case File.stat!(dir) do%{type: :regular, mtime: mtime} ->Map.put(map, dir, mtime)%{type: :directory} ->dir|> Path.join("*")|> Path.wildcard|> files_mtimes(map)%{type: _other} ->mapendend)end@doc falsedef diff(stale_mtimes, fresh_mtimes) dofresh_file_paths = fresh_mtimes |> Map.keys |> MapSet.newstale_file_paths = stale_mtimes |> Map.keys |> MapSet.newcreated_file_paths =MapSet.difference(fresh_file_paths, stale_file_paths) |> MapSet.to_listdeleted_file_paths =MapSet.difference(stale_file_paths, fresh_file_paths) |> MapSet.to_listmodified_file_paths =for file_path <- MapSet.intersection(stale_file_paths, fresh_file_paths),stale_mtimes[file_path] != fresh_mtimes[file_path], do: file_path{created_file_paths, deleted_file_paths, modified_file_paths}enddefp report_change({file_paths, event}, worker_pid) dofor file_path <- file_paths dosend(worker_pid, {:backend_file_event, self(), {file_path, [event]}})endendend
require Loggerdefmodule FileSystem.Backends.FSMac do@moduledoc """This file is a fork from https://github.com/synrc/fs.FileSysetm backend for macos, a GenServer receive data from Port, parse eventand send it to the worker process.Will compile executable the buildin executable file when file the first time it is used.## Backend Options* `:latency` (float, default: 0.5), latency period.* `:no_defer` (bool, default: false), enable no-defer latency modifier.Works with latency parameter, Also check apple `FSEvent` api documentshttps://developer.apple.com/documentation/coreservices/kfseventstreamcreateflagnodefer* `:watch_root` (bool, default: false), watch for when the root path has changed.Set the flag `true` to monitor events when watching `/tmp/fs/dir` and run`mv /tmp/fs /tmp/fx`. Also check apple `FSEvent` api documentshttps://developer.apple.com/documentation/coreservices/kfseventstreamcreateflagwatchroot* recursive is enabled by default, no option to disable it for now.## Executable File PathThe default executable file is `mac_listener` in `priv` dir of `:file_system` application, there're two ways to custom it, useful when run `:file_system` with escript.* config with `config.exs``config :file_system, :fs_mac, executable_file: "YOUR_EXECUTABLE_FILE_PATH"`* config with `FILESYSTEM_FSMAC_EXECUTABLE_FILE` os environmentFILESYSTEM_FSMAC_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH"""use GenServer@behaviour FileSystem.Backend@default_exec_file "mac_listener"def bootstrap doexec_file = executable_path()if not is_nil(exec_file) and File.exists?(exec_file) do:okelseLogger.error "Can't find executable `mac_listener`"{:error, :fs_mac_bootstrap_error}endenddef supported_systems do[{:unix, :darwin}]enddef known_events do[ :mustscansubdirs, :userdropped, :kerneldropped, :eventidswrapped, :historydone,:rootchanged, :mount, :unmount, :created, :removed, :inodemetamod, :renamed, :modified,:finderinfomod, :changeowner, :xattrmod, :isfile, :isdir, :issymlink, :ownevent,]enddefp executable_path doexecutable_path(:system_env) || executable_path(:config) || executable_path(:system_path) || executable_path(:priv)enddefp executable_path(:config) doApplication.get_env(:file_system, :fs_mac)[:executable_file]enddefp executable_path(:system_env) doSystem.get_env("FILESYSTEM_FSMAC_EXECUTABLE_FILE")enddefp executable_path(:system_path) doSystem.find_executable(@default_exec_file)enddefp executable_path(:priv) docase :code.priv_dir(:file_system) do{:error, _} ->Logger.error "`priv` dir for `:file_system` application is not avalible in current runtime, appoint executable file with `config.exs` or `FILESYSTEM_FSMAC_EXECUTABLE_FILE` env."nildir when is_list(dir) ->Path.join(dir, @default_exec_file)endenddef parse_options(options) docase Keyword.pop(options, :dirs) do{nil, _} ->Logger.error "required argument `dirs` is missing"{:error, :missing_dirs_argument}{dirs, rest} ->args = ['-F' | dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)]parse_options(rest, args)endenddefp parse_options([], result), do: {:ok, result}defp parse_options([{:latency, latency} | t], result) doresult =if is_float(latency) or is_integer(latency) do['--latency=#{latency / 1}' | result]elseLogger.error "latency should be integer or float, got `#{inspect latency}, ignore"resultendparse_options(t, result)enddefp parse_options([{:no_defer, true} | t], result) doparse_options(t, ['--no-defer' | result])enddefp parse_options([{:no_defer, false} | t], result) doparse_options(t, result)enddefp parse_options([{:no_defer, value} | t], result) doLogger.error "unknown value `#{inspect value}` for no_defer, ignore"parse_options(t, result)enddefp parse_options([{:with_root, true} | t], result) doparse_options(t, ['--with-root' | result])enddefp parse_options([{:with_root, false} | t], result) doparse_options(t, result)enddefp parse_options([{:with_root, value} | t], result) doLogger.error "unknown value `#{inspect value}` for with_root, ignore"parse_options(t, result)enddefp parse_options([h | t], result) doLogger.error "unknown option `#{inspect h}`, ignore"parse_options(t, result)enddef start_link(args) doGenServer.start_link(__MODULE__, args, [])enddef init(args) do{worker_pid, rest} = Keyword.pop(args, :worker_pid)case parse_options(rest) do{:ok, port_args} ->port = Port.open({:spawn_executable, to_charlist(executable_path())},[:stream, :exit_status, {:line, 16384}, {:args, port_args}, {:cd, System.tmp_dir!()}])Process.link(port)Process.flag(:trap_exit, true){:ok, %{port: port, worker_pid: worker_pid}}{:error, _} ->:ignoreendenddef handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do{file_path, events} = line |> parse_linesend(state.worker_pid, {:backend_file_event, self(), {file_path, events}}){:noreply, state}enddef handle_info({port, {:exit_status, _}}, %{port: port}=state) dosend(state.worker_pid, {:backend_file_event, self(), :stop}){:stop, :normal, state}enddef handle_info({:EXIT, port, _reason}, %{port: port}=state) dosend(state.worker_pid, {:backend_file_event, self(), :stop}){:stop, :normal, state}enddef handle_info(_, state) do{:noreply, state}enddef parse_line(line) do[_, _, events, path] = line |> to_string |> String.split(["\t", "="], parts: 4){path, events |> String.split(["[", ",", "]"], trim: true) |> Enum.map(&String.to_existing_atom/1)}endend
require Loggerdefmodule FileSystem.Backends.FSInotify do@moduledoc """This file is a fork from https://github.com/synrc/fs.FileSystem backend for linux, freebsd and openbsd, a GenServer receive data from Port, parse eventand send it to the worker process.Need `inotify-tools` installed to use this backend.## Backend Options* `:recursive` (bool, default: true), monitor directories and their contents recursively## Executable File PathThe default behaivour to find executable file is finding `inotifywait` from `$PATH`, there're two ways to custom it, useful when run `:file_system` with escript.* config with `config.exs``config :file_system, :fs_inotify, executable_file: "YOUR_EXECUTABLE_FILE_PATH"`* config with `FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE` os environmentFILESYSTEM_FSINOTIFY_EXECUTABLE_FILE=YOUR_EXECUTABLE_FILE_PATH"""use GenServer@behaviour FileSystem.Backend@sep_char <<1>>def bootstrap doexec_file = executable_path()if is_nil(exec_file) doLogger.error "`inotify-tools` is needed to run `file_system` for your system, check https://github.com/rvoicilas/inotify-tools/wiki for more information about how to install it. If it's already installed but not be found, appoint executable file with `config.exs` or `FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE` env."{:error, :fs_inotify_bootstrap_error}else:okendenddef supported_systems do[{:unix, :linux}, {:unix, :freebsd}, {:unix, :openbsd}]enddef known_events do[:created, :deleted, :closed, :modified, :isdir, :attribute, :undefined]enddefp executable_path doexecutable_path(:system_env) || executable_path(:config) || executable_path(:system_path)enddefp executable_path(:config) doApplication.get_env(:file_system, :fs_inotify)[:executable_file]enddefp executable_path(:system_env) doSystem.get_env("FILESYSTEM_FSINOTIFY_EXECUTABLE_FILE")enddefp executable_path(:system_path) doSystem.find_executable("inotifywait")enddef parse_options(options) docase Keyword.pop(options, :dirs) do{nil, _} ->Logger.error "required argument `dirs` is missing"{:error, :missing_dirs_argument}{dirs, rest} ->format = ["%w", "%e", "%f"] |> Enum.join(@sep_char) |> to_charlistargs = ['-e', 'modify', '-e', 'close_write', '-e', 'moved_to', '-e', 'moved_from','-e', 'create', '-e', 'delete', '-e', 'attrib', '--format', format, '--quiet', '-m', '-r'| dirs |> Enum.map(&Path.absname/1) |> Enum.map(&to_charlist/1)]parse_options(rest, args)endenddefp parse_options([], result), do: {:ok, result}defp parse_options([{:recursive, true} | t], result) doparse_options(t, result)enddefp parse_options([{:recursive, false} | t], result) doparse_options(t, result -- ['-r'])enddefp parse_options([{:recursive, value} | t], result) doLogger.error "unknown value `#{inspect value}` for recursive, ignore"parse_options(t, result)enddefp parse_options([h | t], result) doLogger.error "unknown option `#{inspect h}`, ignore"parse_options(t, result)enddef start_link(args) doGenServer.start_link(__MODULE__, args, [])enddef init(args) do{worker_pid, rest} = Keyword.pop(args, :worker_pid)case parse_options(rest) do{:ok, port_args} ->bash_args = ['-c', '#{executable_path()} "$0" "$@" & PID=$!; read a; kill -KILL $PID']all_args =case :os.type() do{:unix, :freebsd} ->bash_args ++ ['--'] ++ port_args_ ->bash_args ++ port_argsendport = Port.open({:spawn_executable, '/bin/sh'},[:stream, :exit_status, {:line, 16384}, {:args, all_args}, {:cd, System.tmp_dir!()}])Process.link(port)Process.flag(:trap_exit, true){:ok, %{port: port, worker_pid: worker_pid}}{:error, _} ->:ignoreendenddef handle_info({port, {:data, {:eol, line}}}, %{port: port}=state) do{file_path, events} = line |> parse_linesend(state.worker_pid, {:backend_file_event, self(), {file_path, events}}){:noreply, state}enddef handle_info({port, {:exit_status, _}}, %{port: port}=state) dosend(state.worker_pid, {:backend_file_event, self(), :stop}){:stop, :normal, state}enddef handle_info({:EXIT, port, _reason}, %{port: port}=state) dosend(state.worker_pid, {:backend_file_event, self(), :stop}){:stop, :normal, state}enddef handle_info(_, state) do{:noreply, state}enddef parse_line(line) do{path, flags} =case line |> to_string |> String.split(@sep_char, trim: true) do[dir, flags, file] -> {Path.join(dir, file), flags}[path, flags] -> {path, flags}end{path, flags |> String.split(",") |> Enum.map(&convert_flag/1)}enddefp convert_flag("CREATE"), do: :createddefp convert_flag("MOVED_TO"), do: :moved_todefp convert_flag("DELETE"), do: :deleteddefp convert_flag("MOVED_FROM"), do: :moved_fromdefp convert_flag("ISDIR"), do: :isdirdefp convert_flag("MODIFY"), do: :modifieddefp convert_flag("CLOSE_WRITE"), do: :modifieddefp convert_flag("CLOSE"), do: :closeddefp convert_flag("ATTRIB"), do: :attributedefp convert_flag(_), do: :undefinedend
require Loggerdefmodule FileSystem.Backend do@moduledoc """FileSystem Backend Behaviour."""@callback bootstrap() :: :ok | {:error, atom()}@callback supported_systems() :: [{atom(), atom()}]@callback known_events() :: [atom()]@doc """Get and validate backend module, return `{:ok, backend_module}` when success andreturn `{:error, reason}` when fail.When `nil` is given, will return default backend by os.When a custom module is given, make sure `start_link/1`, `bootstrap/0` and`supported_system/0` are defnied."""@spec backend(atom) :: {:ok, atom()} | {:error, atom()}def backend(backend) dowith {:ok, module} <- backend_module(backend),:ok <- validate_os(backend, module),:ok <- module.bootstrapdo{:ok, module}else{:error, reason} -> {:error, reason}endenddefp backend_module(nil) docase :os.type() do{:unix, :darwin} -> :fs_mac{:unix, :linux} -> :fs_inotify{:unix, :freebsd} -> :fs_inotify{:unix, :openbsd} -> :fs_inotify{:win32, :nt} -> :fs_windowssystem -> {:unsupported_system, system}end |> backend_moduleenddefp backend_module(:fs_mac), do: {:ok, FileSystem.Backends.FSMac}defp backend_module(:fs_inotify), do: {:ok, FileSystem.Backends.FSInotify}defp backend_module(:fs_windows), do: {:ok, FileSystem.Backends.FSWindows}defp backend_module(:fs_poll), do: {:ok, FileSystem.Backends.FSPoll}defp backend_module({:unsupported_system, system}) doLogger.error "I'm so sorry but `file_system` does NOT support your current system #{inspect system} for now."{:error, :unsupported_system}enddefp backend_module(module) dofunctions = module.__info__(:functions){:start_link, 1} in functions &&{:bootstrap, 0} in functions &&{:supported_systems, 0} in functions ||raise "illegal backend"rescue_ ->Logger.error "You are using custom backend `#{inspect module}`, make sure it's a legal file_system backend module."{:error, :illegal_backend}enddefp validate_os(backend, module) doos_type = :os.type()if os_type in module.supported_systems() do:okelseLogger.error "The backend `#{backend}` you are using does NOT support your current system #{inspect os_type}."{:error, :unsupported_system}endendend
{<<"app">>,<<"file_system">>}.{<<"build_tools">>,[<<"mix">>]}.{<<"description">>,<<"A file system change watcher wrapper based on [fs](https://github.com/synrc/fs)">>}.{<<"elixir">>,<<"~> 1.3">>}.{<<"files">>,[<<"lib">>,<<"lib/file_system">>,<<"lib/file_system/worker.ex">>,<<"lib/file_system/backends">>,<<"lib/file_system/backends/fs_inotify.ex">>,<<"lib/file_system/backends/fs_mac.ex">>,<<"lib/file_system/backends/fs_poll.ex">>,<<"lib/file_system/backends/fs_windows.ex">>,<<"lib/file_system/backend.ex">>,<<"lib/file_system.ex">>,<<"README.md">>,<<"mix.exs">>,<<"c_src/mac/cli.c">>,<<"c_src/mac/cli.h">>,<<"c_src/mac/common.h">>,<<"c_src/mac/compat.c">>,<<"c_src/mac/compat.h">>,<<"c_src/mac/main.c">>,<<"priv/inotifywait.exe">>]}.{<<"licenses">>,[<<"WTFPL">>]}.{<<"links">>,[{<<"Github">>,<<"https://github.com/falood/file_system">>}]}.{<<"name">>,<<"file_system">>}.{<<"requirements">>,[]}.{<<"version">>,<<"0.2.10">>}.
#include "common.h"#include "cli.h"// TODO: set on fire. cli.{h,c} handle both parsing and defaults, so there's// no need to set those here. also, in order to scope metadata by path,// each stream will need its own configuration... so this won't work as// a global any more. In the end the goal is to make the output format// able to declare not just that something happened and what flags were// attached, but what path it was watching that caused those events (so// that the path itself can be used for routing that information to the// relevant callback).//// Structure for storing metadata parsed from the commandlinestatic struct {FSEventStreamEventId sinceWhen;CFTimeInterval latency;FSEventStreamCreateFlags flags;CFMutableArrayRef paths;int format;} config = {(UInt64) kFSEventStreamEventIdSinceNow,(double) 0.3,(CFOptionFlags) kFSEventStreamCreateFlagNone,NULL,0};// Prototypesstatic void append_path(const char* path);static inline void parse_cli_settings(int argc, const char* argv[]);static void callback(FSEventStreamRef streamRef,void* clientCallBackInfo,size_t numEvents,void* eventPaths,const FSEventStreamEventFlags eventFlags[],const FSEventStreamEventId eventIds[]);static void append_path(const char* path){CFStringRef pathRef = CFStringCreateWithCString(kCFAllocatorDefault,path,kCFStringEncodingUTF8);CFArrayAppendValue(config.paths, pathRef);CFRelease(pathRef);}// Parse commandline settingsstatic inline void parse_cli_settings(int argc, const char* argv[]){// runtime os version detectionSInt32 osMajorVersion, osMinorVersion;if (!(Gestalt(gestaltSystemVersionMajor, &osMajorVersion) == noErr)) {osMajorVersion = 0;}if (!(Gestalt(gestaltSystemVersionMinor, &osMinorVersion) == noErr)) {osMinorVersion = 0;}if ((osMajorVersion == 10) & (osMinorVersion < 5)) {fprintf(stderr, "The FSEvents API is unavailable on this version of macos!\n");exit(EXIT_FAILURE);}struct cli_info args_info;cli_parser_init(&args_info);if (cli_parser(argc, argv, &args_info) != 0) {exit(EXIT_FAILURE);}config.paths = CFArrayCreateMutable(NULL,(CFIndex)0,&kCFTypeArrayCallBacks);config.sinceWhen = args_info.since_when_arg;config.latency = args_info.latency_arg;config.format = args_info.format_arg;if (args_info.no_defer_flag) {config.flags |= kFSEventStreamCreateFlagNoDefer;}if (args_info.watch_root_flag) {config.flags |= kFSEventStreamCreateFlagWatchRoot;}if (args_info.ignore_self_flag) {if ((osMajorVersion > 10) | ((osMajorVersion == 10) & (osMinorVersion >= 6))) {config.flags |= kFSEventStreamCreateFlagIgnoreSelf;} else {fprintf(stderr, "MacOSX 10.6 or later is required for --ignore-self\n");exit(EXIT_FAILURE);}}if (args_info.file_events_flag) {if ((osMajorVersion > 10) | ((osMajorVersion == 10) & (osMinorVersion >= 7))) {config.flags |= kFSEventStreamCreateFlagFileEvents;} else {fprintf(stderr, "MacOSX 10.7 or later required for --file-events\n");exit(EXIT_FAILURE);}}if (args_info.mark_self_flag) {if ((osMajorVersion > 10) | ((osMajorVersion == 10) & (osMinorVersion >= 9))) {config.flags |= kFSEventStreamCreateFlagMarkSelf;} else {fprintf(stderr, "MacOSX 10.9 or later required for --mark-self\n");exit(EXIT_FAILURE);}}if (args_info.inputs_num == 0) {append_path(".");} else {for (unsigned int i=0; i < args_info.inputs_num; ++i) {append_path(args_info.inputs[i]);}}cli_parser_free(&args_info);#ifdef DEBUGfprintf(stderr, "config.sinceWhen %llu\n", config.sinceWhen);fprintf(stderr, "config.latency %f\n", config.latency);fprintf(stderr, "config.flags %#.8x\n", config.flags);FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagUseCFTypes," Using CF instead of C types");FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagNoDefer," NoDefer latency modifier enabled");FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagWatchRoot," WatchRoot notifications enabled");FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagIgnoreSelf," IgnoreSelf enabled");FLAG_CHECK_STDERR(config.flags, kFSEventStreamCreateFlagFileEvents," FileEvents enabled");fprintf(stderr, "config.paths\n");long numpaths = CFArrayGetCount(config.paths);for (long i = 0; i < numpaths; i++) {char path[PATH_MAX];CFStringGetCString(CFArrayGetValueAtIndex(config.paths, i),path,PATH_MAX,kCFStringEncodingUTF8);fprintf(stderr, " %s\n", path);}fprintf(stderr, "\n");#endif}static void callback(__attribute__((unused)) FSEventStreamRef streamRef,__attribute__((unused)) void* clientCallBackInfo,size_t numEvents,void* eventPaths,const FSEventStreamEventFlags eventFlags[],const FSEventStreamEventId eventIds[]){char** paths = eventPaths;char *buf = calloc(sizeof(FSEVENTSBITS), sizeof(char));for (size_t i = 0; i < numEvents; i++) {sprintb(buf, eventFlags[i], FSEVENTSBITS);printf("%llu\t%#.8x=[%s]\t%s\n", eventIds[i], eventFlags[i], buf, paths[i]);}fflush(stdout);free(buf);if (fcntl(STDIN_FILENO, F_GETFD) == -1) {CFRunLoopStop(CFRunLoopGetCurrent());}}static void stdin_callback(CFFileDescriptorRef fdref, CFOptionFlags callBackTypes, void *info){char buf[1024];int nread;do {nread = read(STDIN_FILENO, buf, sizeof(buf));if (nread == -1 && errno == EAGAIN) {CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);return;} else if (nread == 0) {exit(1);return;}} while (nread > 0);}int main(int argc, const char* argv[]){parse_cli_settings(argc, argv);FSEventStreamContext context = {0, NULL, NULL, NULL, NULL};FSEventStreamRef stream;stream = FSEventStreamCreate(kCFAllocatorDefault,(FSEventStreamCallback)&callback,&context,config.paths,config.sinceWhen,config.latency,config.flags);#ifdef DEBUGFSEventStreamShow(stream);fprintf(stderr, "\n");#endiffcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK);CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, STDIN_FILENO, false, stdin_callback, NULL);CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);CFRelease(source);FSEventStreamScheduleWithRunLoop(stream,CFRunLoopGetCurrent(),kCFRunLoopDefaultMode);FSEventStreamStart(stream);CFRunLoopRun();FSEventStreamFlushSync(stream);FSEventStreamStop(stream);return 0;}// vim: ts=2 sts=2 et sw=2
/*** @headerfile compat.h* FSEventStream flag compatibility shim** In order to compile a binary against an older SDK yet still support the* features present in later OS releases, we need to define any missing enum* constants not present in the older SDK. This allows us to safely defer* feature detection to runtime (and avoid recompilation).*/#ifndef fsevent_watch_compat_h#define fsevent_watch_compat_h#ifndef __CORESERVICES__#include <CoreServices/CoreServices.h>#endif // __CORESERVICES__#if MAC_OS_X_VERSION_MAX_ALLOWED < 1060// ignoring events originating from the current process introduced in 10.6extern FSEventStreamCreateFlags kFSEventStreamCreateFlagIgnoreSelf;#endif#if MAC_OS_X_VERSION_MAX_ALLOWED < 1070// file-level events introduced in 10.7extern FSEventStreamCreateFlags kFSEventStreamCreateFlagFileEvents;extern FSEventStreamEventFlags kFSEventStreamEventFlagItemCreated,kFSEventStreamEventFlagItemRemoved,kFSEventStreamEventFlagItemInodeMetaMod,kFSEventStreamEventFlagItemRenamed,kFSEventStreamEventFlagItemModified,kFSEventStreamEventFlagItemFinderInfoMod,kFSEventStreamEventFlagItemChangeOwner,kFSEventStreamEventFlagItemXattrMod,kFSEventStreamEventFlagItemIsFile,kFSEventStreamEventFlagItemIsDir,kFSEventStreamEventFlagItemIsSymlink;#endif#if MAC_OS_X_VERSION_MAX_ALLOWED < 1090// marking, rather than ignoring, events originating from the current process introduced in 10.9extern FSEventStreamCreateFlags kFSEventStreamCreateFlagMarkSelf;extern FSEventStreamEventFlags kFSEventStreamEventFlagOwnEvent;#endif#endif // fsevent_watch_compat_h
#include "compat.h"#if MAC_OS_X_VERSION_MAX_ALLOWED < 1060FSEventStreamCreateFlags kFSEventStreamCreateFlagIgnoreSelf = 0x00000008;#endif#if MAC_OS_X_VERSION_MAX_ALLOWED < 1070FSEventStreamCreateFlags kFSEventStreamCreateFlagFileEvents = 0x00000010;FSEventStreamEventFlags kFSEventStreamEventFlagItemCreated = 0x00000100;FSEventStreamEventFlags kFSEventStreamEventFlagItemRemoved = 0x00000200;FSEventStreamEventFlags kFSEventStreamEventFlagItemInodeMetaMod = 0x00000400;FSEventStreamEventFlags kFSEventStreamEventFlagItemRenamed = 0x00000800;FSEventStreamEventFlags kFSEventStreamEventFlagItemModified = 0x00001000;FSEventStreamEventFlags kFSEventStreamEventFlagItemFinderInfoMod = 0x00002000;FSEventStreamEventFlags kFSEventStreamEventFlagItemChangeOwner = 0x00004000;FSEventStreamEventFlags kFSEventStreamEventFlagItemXattrMod = 0x00008000;FSEventStreamEventFlags kFSEventStreamEventFlagItemIsFile = 0x00010000;FSEventStreamEventFlags kFSEventStreamEventFlagItemIsDir = 0x00020000;FSEventStreamEventFlags kFSEventStreamEventFlagItemIsSymlink = 0x00040000;#endif#if MAC_OS_X_VERSION_MAX_ALLOWED < 1090FSEventStreamCreateFlags kFSEventStreamCreateFlagMarkSelf = 0x00000020;FSEventStreamEventFlags kFSEventStreamEventFlagOwnEvent = 0x00080000;#endif
#ifndef fsevent_watch_common_h#define fsevent_watch_common_h#include <CoreFoundation/CoreFoundation.h>#include <CoreServices/CoreServices.h>#include <unistd.h>#include <fcntl.h>#include "compat.h"#define _str(s) #s#define _xstr(s) _str(s)#define COMPILED_AT __DATE__ " " __TIME__#define FPRINTF_FLAG_CHECK(flags, flag, msg, fd) \do { \if ((flags) & (flag)) { \fprintf(fd, "%s\n", msg); } } \while (0)#define FLAG_CHECK_STDERR(flags, flag, msg) \FPRINTF_FLAG_CHECK(flags, flag, msg, stderr)/** FSEVENTSBITS:* generated by `make printflags` (and pasted here)* flags MUST be ordered (bits ascending) and sorted** idea from: http://www.openbsd.org/cgi-bin/cvsweb/src/sbin/ifconfig/ifconfig.c (see printb())*/#define FSEVENTSBITS \"\1mustscansubdirs\2userdropped\3kerneldropped\4eventidswrapped\5historydone\6rootchanged\7mount\10unmount\11created\12removed\13inodemetamod\14renamed\15modified\16finderinfomod\17changeowner\20xattrmod\21isfile\22isdir\23issymlink\24ownevent"static inline voidsprintb(char *buf, unsigned short v, char *bits){int i, any = 0;char c;char *bufp = buf;while ((i = *bits++)) {if (v & (1 << (i-1))) {if (any)*bufp++ = ',';any = 1;for (; (c = *bits) > 32; bits++)*bufp++ = c;} elsefor (; *bits > 32; bits++);}*bufp = '\0';}#endif /* fsevent_watch_common_h */
#ifndef CLI_H#define CLI_H#include "common.h"#ifndef CLI_NAME#define CLI_NAME "fsevent_watch"#endif /* CLI_NAME */struct cli_info {UInt64 since_when_arg;double latency_arg;bool no_defer_flag;bool watch_root_flag;bool ignore_self_flag;bool file_events_flag;bool mark_self_flag;int format_arg;char** inputs;unsigned inputs_num;};extern const char* cli_info_purpose;extern const char* cli_info_usage;extern const char* cli_info_help[];void cli_print_help(void);void cli_print_version(void);int cli_parser (int argc, const char** argv, struct cli_info* args_info);void cli_parser_init (struct cli_info* args_info);void cli_parser_free (struct cli_info* args_info);#endif /* CLI_H */
#include <getopt.h>#include "cli.h"const char* cli_info_purpose = "A flexible command-line interface for the FSEvents API";const char* cli_info_usage = "Usage: fsevent_watch [OPTIONS]... [PATHS]...";const char* cli_info_help[] = {" -h, --help you're looking at it"," -V, --version print version number and exit"," -p, --show-plist display the embedded Info.plist values"," -s, --since-when=EventID fire historical events since ID"," -l, --latency=seconds latency period (default='0.5')"," -n, --no-defer enable no-defer latency modifier"," -r, --watch-root watch for when the root path has changed",// " -i, --ignore-self ignore current process"," -F, --file-events provide file level event data"," -f, --format=name output format (ignored)",0};static void default_args (struct cli_info* args_info){args_info->since_when_arg = kFSEventStreamEventIdSinceNow;args_info->latency_arg = 0.5;args_info->no_defer_flag = false;args_info->watch_root_flag = false;args_info->ignore_self_flag = false;args_info->file_events_flag = false;args_info->mark_self_flag = false;args_info->format_arg = 0;}static void cli_parser_release (struct cli_info* args_info){unsigned int i;for (i=0; i < args_info->inputs_num; ++i) {free(args_info->inputs[i]);}if (args_info->inputs_num) {free(args_info->inputs);}args_info->inputs_num = 0;}void cli_parser_init (struct cli_info* args_info){default_args(args_info);args_info->inputs = 0;args_info->inputs_num = 0;}void cli_parser_free (struct cli_info* args_info){cli_parser_release(args_info);}static void cli_print_info_dict (const void *key,const void *value,void *context){CFStringRef entry = CFStringCreateWithFormat(NULL, NULL,CFSTR("%@:\n %@"), key, value);if (entry) {CFShow(entry);CFRelease(entry);}}void cli_show_plist (void){CFBundleRef mainBundle = CFBundleGetMainBundle();CFRetain(mainBundle);CFDictionaryRef mainBundleDict = CFBundleGetInfoDictionary(mainBundle);if (mainBundleDict) {CFRetain(mainBundleDict);printf("Embedded Info.plist metadata:\n\n");CFDictionaryApplyFunction(mainBundleDict, cli_print_info_dict, NULL);CFRelease(mainBundleDict);}CFRelease(mainBundle);printf("\n");}void cli_print_version (void){printf("%s %s\n\n", "VXZ", "1.0");}void cli_print_help (void){cli_print_version();printf("\n%s\n", cli_info_purpose);printf("\n%s\n", cli_info_usage);printf("\n");int i = 0;while (cli_info_help[i]) {printf("%s\n", cli_info_help[i++]);}}int cli_parser (int argc, const char** argv, struct cli_info* args_info){static struct option longopts[] = {{ "help", no_argument, NULL, 'h' },{ "version", no_argument, NULL, 'V' },{ "show-plist", no_argument, NULL, 'p' },{ "since-when", required_argument, NULL, 's' },{ "latency", required_argument, NULL, 'l' },{ "no-defer", no_argument, NULL, 'n' },{ "watch-root", no_argument, NULL, 'r' },{ "ignore-self", no_argument, NULL, 'i' },{ "file-events", no_argument, NULL, 'F' },{ "mark-self", no_argument, NULL, 'm' },{ "format", required_argument, NULL, 'f' },{ 0, 0, 0, 0 }};const char* shortopts = "hVps:l:nriFf:";int c = -1;while ((c = getopt_long(argc, (char * const*)argv, shortopts, longopts, NULL)) != -1) {switch(c) {case 's': // since-whenargs_info->since_when_arg = strtoull(optarg, NULL, 0);break;case 'l': // latencyargs_info->latency_arg = strtod(optarg, NULL);break;case 'n': // no-deferargs_info->no_defer_flag = true;break;case 'r': // watch-rootargs_info->watch_root_flag = true;break;case 'i': // ignore-selfargs_info->ignore_self_flag = true;break;case 'F': // file-eventsargs_info->file_events_flag = true;break;case 'm': // mark-selfargs_info->mark_self_flag = true;break;case 'f': // format// XXX: ignoredbreak;case 'V': // versioncli_print_version();exit(EXIT_SUCCESS);case 'p': // show-plistcli_show_plist();exit(EXIT_SUCCESS);case 'h': // helpcase '?': // invalid optioncase ':': // missing argumentcli_print_help();exit((c == 'h') ? EXIT_SUCCESS : EXIT_FAILURE);}}if (optind < argc) {int i = 0;args_info->inputs_num = (unsigned int)(argc - optind);args_info->inputs =(char**)(malloc ((args_info->inputs_num)*sizeof(char*)));while (optind < argc)if (argv[optind++] != argv[0]) {args_info->inputs[i++] = strdup(argv[optind-1]);}}return EXIT_SUCCESS;}
FileSystem=========A file change watcher wrapper based on [fs](https://github.com/synrc/fs)## System Support- Mac fsevent- Linux, FreeBSD and OpenBSD inotify- Windows inotify-winNOTE:On Linux, FreeBSD and OpenBSD you need to install inotify-tools.On Macos 10.14, you need run `open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg` to compile `mac_listener`.## UsagePut `file_system` in the `deps` and `application` part of your mix.exs``` elixirdefmodule Excellent.Mixfile douse Mix.Projectdef project do...enddefp deps do[{ :file_system, "~> 0.2", only: :test },]end...end```### Subscription APIYou can spawn a worker and subscribe to events from it:```elixir{:ok, pid} = FileSystem.start_link(dirs: ["/path/to/some/files"])FileSystem.subscribe(pid)```or```elixir{:ok, pid} = FileSystem.start_link(dirs: ["/path/to/some/files"], name: :my_monitor_name)FileSystem.subscribe(:my_monitor_name)```The pid you subscribed from will now receive messages like```{:file_event, worker_pid, {file_path, events}}```and```{:file_event, worker_pid, :stop}```### Example with GenServer```elixirdefmodule Watcher douse GenServerdef start_link(args) doGenServer.start_link(__MODULE__, args)enddef init(args) do{:ok, watcher_pid} = FileSystem.start_link(args)FileSystem.subscribe(watcher_pid){:ok, %{watcher_pid: watcher_pid}}enddef handle_info({:file_event, watcher_pid, {path, events}}, %{watcher_pid: watcher_pid}=state) do# YOUR OWN LOGIC FOR PATH AND EVENTS{:noreply, state}enddef handle_info({:file_event, watcher_pid, :stop}, %{watcher_pid: watcher_pid}=state) do# YOUR OWN LOGIC WHEN MONITOR STOP{:noreply, state}endend```## Tweaking behaviour via extra argumentsFor each platform, you can pass extra arguments to the underlying listener process.Each backend support different extra arguments, check backend module documentation for more information.Here is an example to get instant notifications on file changes for Mac OS X:```elixirFileSystem.start_link(dirs: ["/path/to/some/files"], latency: 0, watch_root: true)```
{application,custom_customs,[{applications,[kernel,stdlib,elixir,logger]},{description,"custom_customs"},{modules,['Elixir.CustomCustoms']},{registered,[]},{vsn,"0.1.0"}]}.
{application,mix_test_watch,[{applications,[kernel,stdlib,elixir,file_system]},{description,"Automatically run tests when files change"},{modules,['Elixir.Mix.Tasks.Test.Watch','Elixir.MixTestWatch','Elixir.MixTestWatch.Config','Elixir.MixTestWatch.MessageInbox','Elixir.MixTestWatch.Path','Elixir.MixTestWatch.PortRunner','Elixir.MixTestWatch.Runner','Elixir.MixTestWatch.Watcher']},{registered,[]},{vsn,"1.0.2"},{mod,{'Elixir.MixTestWatch',[]}}]}.
{application,file_system,[{applications,[kernel,stdlib,elixir,logger]},{description,"A file system change watcher wrapper based on [fs](https://github.com/synrc/fs)"},{modules,['Elixir.FileSystem','Elixir.FileSystem.Backend','Elixir.FileSystem.Backends.FSInotify','Elixir.FileSystem.Backends.FSMac','Elixir.FileSystem.Backends.FSPoll','Elixir.FileSystem.Backends.FSWindows','Elixir.FileSystem.Worker']},{registered,[]},{vsn,"0.2.10"}]}.
{application,custom_customs,[{applications,[kernel,stdlib,elixir,logger]},{description,"custom_customs"},{modules,['Elixir.CustomCustoms']},{registered,[]},{vsn,"0.1.0"}]}.
# CustomCustoms**TODO: Add description**## InstallationIf [available in Hex](https://hex.pm/docs/publish), the package can be installedby adding `custom_customs` to your list of dependencies in `mix.exs`:```elixirdef deps do[{:custom_customs, "~> 0.1.0"}]end```Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)and published on [HexDocs](https://hexdocs.pm). Once published, the docs canbe found at [https://hexdocs.pm/custom_customs](https://hexdocs.pm/custom_customs).